TypeStack/routing-controllers для TypeScript: полное руководство с примерами
1. Что это и зачем нужна
TypeStack/routing-controllers — это мощная библиотека для TypeScript, которая позволяет создавать серверные приложения на Node.js с использованием декораторов. Она предоставляет декларативный способ определения маршрутов (routes), middleware, валидации и сериализации данных. Вместо того чтобы вручную писать обработчики для каждого HTTP-запроса и связывать их с Express или Koa, вы просто аннотируете классы и методы декораторами, а библиотека автоматически генерирует всю необходимую инфраструктуру.
Зачем это нужно? В современной веб-разработке на TypeScript часто возникает проблема дублирования кода и сложности поддержки больших проектов. routing-controllers решает эту проблему, предлагая чистую, модульную архитектуру, основанную на классах и декораторах. Это делает код более читаемым, тестируемым и масштабируемым. Вы можете легко переиспользовать middleware, валидировать входящие данные с помощью class-validator и автоматически генерировать OpenAPI (Swagger) документацию.
Библиотека тесно интегрируется с популярными фреймворками, такими как Express и Koa, и является частью экосистемы TypeStack, которая также включает в себя class-validator, class-transformer и typedi. Это позволяет строить полноценные backend-приложения с инверсией зависимостей, валидацией и трансформацией данных "из коробки".
2. Установка
Для установки библиотеки и её зависимостей выполните следующие команды в терминале вашего проекта:
npm install routing-controllers reflect-metadata
Также вам понадобится установить один из поддерживаемых HTTP-фреймворков. Рекомендуется Express или Koa:
npm install express
npm install @types/express --save-dev
Или для Koa:
npm install koa
npm install @types/koa --save-dev
Не забудьте включить экспериментальные декораторы в вашем tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}
3. Быстрый старт — минимальный рабочий пример
Создайте файл app.ts со следующим содержимым:
import 'reflect-metadata';
import { createExpressServer, Get, Controller, Res } from 'routing-controllers';
import { Response } from 'express';
@Controller()
class UserController {
@Get('/users')
getAll(@Res() response: Response) {
return response.json([{ id: 1, name: 'John Doe' }]);
}
}
const app = createExpressServer({
controllers: [UserController],
});
app.listen(3000, () => {
console.log('Server is running on http://localhost:3000');
});
Запустите сервер командой npx ts-node app.ts и откройте в браузере http://localhost:3000/users. Вы увидите JSON-ответ с массивом пользователей.
4. Основные методы/функции/классы
4.1 @Controller
Сигнатура: @Controller(basePath?: string)
Что делает: Декоратор класса, который помечает класс как контроллер. Принимает необязательный базовый путь, который будет добавлен ко всем маршрутам внутри класса.
Рабочий пример:
@Controller('/api')
class ApiController {
@Get('/status')
getStatus() {
return { status: 'OK' };
}
}
// Маршрут: GET /api/status
4.2 @Get, @Post, @Put, @Delete, @Patch
Сигнатура: @Get(route: string) (аналогично для других HTTP методов)
Что делает: Декораторы методов, которые привязывают метод класса к определённому HTTP-методу и маршруту.
Рабочий пример:
@Controller('/items')
class ItemController {
@Get('/')
getAll() { return []; }
@Post('/')
create() { return { created: true }; }
@Put('/:id')
update(@Param('id') id: number) { return { updated: id }; }
@Delete('/:id')
delete(@Param('id') id: number) { return { deleted: id }; }
}
4.3 @Param
Сигнатура: @Param(name: string)
Что делает: Извлекает параметр из URL-маршрута и передаёт его в аргумент метода. Автоматически преобразует типы (например, строку в число).
Рабочий пример:
@Get('/users/:userId/posts/:postId')
getPost(
@Param('userId') userId: number,
@Param('postId') postId: number
) {
return `User ${userId}, Post ${postId}`;
}
4.4 @Body
Сигнатура: @Body(options?: BodyOptions)
Что делает: Извлекает тело запроса (request body) и передаёт его в аргумент метода. Может автоматически валидировать и трансформировать данные с помощью class-validator и class-transformer.
Рабочий пример:
class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
}
@Post('/users')
createUser(@Body() userData: CreateUserDto) {
// userData уже валидирован и типизирован
return userData;
}
4.5 @QueryParam
Сигнатура: @QueryParam(name: string)
Что делает: Извлекает параметр из query-строки URL (например, ?page=1&limit=10).
Рабочий пример:
@Get('/search')
search(
@QueryParam('q') query: string,
@QueryParam('page') page: number = 1
) {
return `Search: ${query}, Page: ${page}`;
}
4.6 @HeaderParam
Сигнатура: @HeaderParam(name: string)
Что делает: Извлекает значение из HTTP-заголовка запроса.
Рабочий пример:
@Get('/protected')
protectedRoute(@HeaderParam('authorization') authHeader: string) {
if (!authHeader) {
throw new Error('Authorization header is required');
}
return { authorized: true };
}
4.7 @UseBefore и @UseAfter
Сигнатура: @UseBefore(...middlewares: Function[]) / @UseAfter(...middlewares: Function[])
Что делает: Позволяют применить middleware к контроллеру или конкретному методу. @UseBefore выполняется до обработчика, @UseAfter — после.
Рабочий пример:
function loggingMiddleware(req: any, res: any, next: Function) {
console.log(`Request: ${req.method} ${req.path}`);
next();
}
@Controller('/logs')
@UseBefore(loggingMiddleware)
class LogController {
@Get('/')
getAll() {
return { message: 'Logged' };
}
}
4.8 @OnUndefined и @OnNull
Сигнатура: @OnUndefined(code: number) / @OnNull(code: number)
Что делает: Указывает HTTP-статус код, который будет возвращён, если метод возвращает undefined или null. Полезно для обработки случаев "не найдено".
Рабочий пример:
@Get('/users/:id')
@OnUndefined(404)
getUser(@Param('id') id: number) {
const user = findUserById(id);
if (!user) return undefined;
return user;
}
5. Реальный практический пример (100-200 строк кода)
Создадим полноценный REST API для управления задачами (Todo List) с валидацией и обработкой ошибок.
import 'reflect-metadata';
import {
createExpressServer,
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
QueryParam,
OnUndefined,
HttpCode,
JsonController,
BadRequestError,
NotFoundError
} from 'routing-controllers';
import { IsString, IsBoolean, IsOptional, MinLength } from 'class-validator';
import { validate } from 'class-validator';
// DTO (Data Transfer Object) для создания задачи
class CreateTodoDto {
@IsString()
@MinLength(3, { message: 'Title must be at least 3 characters' })
title: string;
@IsOptional()
@IsBoolean()
completed?: boolean;
}
// DTO для обновления задачи
class UpdateTodoDto {
@IsOptional()
@IsString()
@MinLength(3)
title?: string;
@IsOptional()
@IsBoolean()
completed?: boolean;
}
// Интерфейс задачи
interface Todo {
id: number;
title: string;
completed: boolean;
createdAt: Date;
}
// Хранилище данных (в реальном проекте замените на БД)
let todos: Todo[] = [];
let nextId = 1;
@JsonController('/todos')
export class TodoController {
@Get('/')
async getAll(
@QueryParam('completed') completed?: boolean
): Promise {
if (completed !== undefined) {
return todos.filter(t => t.completed === completed);
}
return todos;
}
@Get('/:id')
@OnUndefined(404)
async getOne(@Param('id') id: number): Promise {
return todos.find(t => t.id === id);
}
@Post('/')
@HttpCode(201)
async create(@Body({ validate: true }) todoData: CreateTodoDto): Promise {
const newTodo: Todo = {
id: nextId++,
title: todoData.title,
completed: todoData.completed || false,
createdAt: new Date()
};
todos.push(newTodo);
return newTodo;
}
@Put('/:id')
async update(
@Param('id') id: number,
@Body({ validate: true }) todoData: UpdateTodoDto
): Promise {
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
throw new NotFoundError(`Todo with id ${id} not found`);
}
const existingTodo = todos[todoIndex];
const updatedTodo: Todo = {
...existingTodo,
title: todoData.title ?? existingTodo.title,
completed: todoData.completed ?? existingTodo.completed
};
todos[todoIndex] = updatedTodo;
return updatedTodo;
}
@Delete('/:id')
@HttpCode(204)
async delete(@Param('id') id: number): Promise {
const todoIndex = todos.findIndex(t => t.id === id);
if (todoIndex === -1) {
throw new NotFoundError(`Todo with id ${id} not found`);
}
todos.splice(todoIndex, 1);
}
}
// Создаём и запускаем сервер
const app = createExpressServer({
controllers: [TodoController],
// Глобальная обработка ошибок
defaultErrorHandler: false,
});
// Пользовательский обработчик ошибок
app.use((err: any, req: any, res: any, next: Function) => {
if (err instanceof BadRequestError) {
return res.status(400).json({
error: 'Validation Error',
details: err.errors || err.message
});
}
if (err instanceof NotFoundError) {
return res.status(404).json({ error: err.message });
}
console.error('Unhandled error:', err);
res.status(500).json({ error: 'Internal Server Error' });
});
app.listen(3000, () => {
console.log('Todo API running on http://localhost:3000/todos');
});
6. Советы по производительности и типичные ошибки
Советы по производительности:
- Используйте
JsonControllerвместоController— он автоматически сериализует ответы в JSON и устанавливает правильные заголовки, что избавляет от лишних вызововresponse.json(). - Включайте валидацию только там, где это необходимо. Используйте опцию
validate: trueв декораторе@Bodyтолько для тех маршрутов, где это действительно нужно. Для простых GET-запросов валидация может быть избыточной. - Избегайте излишнего использования middleware. Каждый middleware добавляет накладные расходы. Группируйте middleware на уровне контроллера, если они применяются ко всем маршрутам.
- Используйте кэширование для часто запрашиваемых данных. Например, для списка задач можно добавить простой in-memory кэш с TTL.
- Настройте
class-transformerдля оптимальной сериализации. Используйте опцииexcludeExtraneousValuesиenableCircularCheckдля уменьшения размера ответов.
Типичные ошибки:
- Забыли импортировать
reflect-metadata. Без этого импорта декораторы не будут работать. Всегда добавляйтеimport 'reflect-metadata'в самом начале вашего главного файла. - Неправильная на