TypeStack/routing-controllers для TypeScript: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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' в самом начале вашего главного файла.
  • Неправильная на

Рекомендаций