Skip to main content

Що таке контролери в NestJS?

Controllers (контролери) в NestJS - це класи, які приймають HTTP запити і повертають відповіді. Декоратори прив'язують URL-шляхи та HTTP методи до конкретних функцій-обробників.

Теорія

TL;DR

  • Контролер - точка входу для HTTP: отримує запит, витягує дані, викликає сервіс, повертає відповідь
  • @Controller('users') задає префікс маршруту; @Get(), @Post() прив'язують HTTP методи до методів класу
  • Контролер відповідає за HTTP (маршрути, статус-коди, заголовки); бізнес-логіка йде в сервіси
  • Правило: якщо це HTTP - в контролер; якщо бізнес-логіка - в сервіс

Швидкий приклад

typescript
import { Controller, Get, Post, Body, Param } from '@nestjs/common'; import { UsersService } from './users.service'; @Controller('users') // всі маршрути починаються з /users export class UsersController { constructor(private usersService: UsersService) {} @Get(':id') // GET /users/123 getUser(@Param('id') id: string) { return this.usersService.findById(id); // делегує сервісу } @Post() // POST /users createUser(@Body() userData: CreateUserDto) { return this.usersService.create(userData); // контролер сам роботу не виконує } } // Запит → контролер витягує дані → сервіс виконує роботу → відповідь

Контролер отримав запит, витягнув потрібні дані (@Param, @Body) і передав їх сервісу. Це вся його робота.

Контролер vs сервіс

Контролери - це HTTP boundary layer, шар на межі між HTTP і бізнес-логікою. Вони перекладають HTTP запити у виклики функцій і повертають результати назад як HTTP відповіді. Уяви контролер як рецепціоніста: приймає запит, скеровує до потрібного відділу (сервісу) і передає відповідь назад. Рецепціоніст сам роботу не виконує.

Сервіси містять бізнес-логіку і можуть викликатися з будь-якого контексту: HTTP, WebSockets, scheduled jobs, CLI. Контролери прив'язані до HTTP. Сервіси - ні.

Коли використовувати

  • Маршрутизація - прив'язати URL та HTTP методи до функцій
  • Витягування даних - отримати дані з @Param(), @Query(), @Body(), @Headers()
  • Контроль відповіді - встановити статус через @HttpCode(), управляти заголовками через @Res()
  • Guards та interceptors - застосувати авторизацію або логування на рівні маршруту
  • НЕ для запитів до бази даних, обчислень або будь-якої доменної логіки

Як це працює всередині

Коли приходить запит, NestJS зіставляє URL і HTTP метод з методом контролера через декоратори. Фреймворк автоматично витягує path params, query string і body, потім викликає метод. Все що метод повертає, серіалізується в JSON і відправляється зі статусом 200 за замовчуванням. @HttpCode(HttpStatus.CREATED) змінює статус на 201. @Res() дає прямий контроль над об'єктом відповіді.

Поширені помилки

Бізнес-логіка в контролері:

typescript
// Неправильно: контролер звертається до бази даних і обчислює поля @Get(':id') getUser(@Param('id') id: string) { const user = this.db.query('SELECT * FROM users WHERE id = ?', [id]); return { ...user, age: new Date().getFullYear() - user.birthYear }; } // Правильно: контролер делегує все сервісу @Get(':id') getUser(@Param('id') id: string) { return this.usersService.getUserWithEnrichment(id); }

Логіка в контролерах не може використовуватися з WebSockets або scheduled jobs, і її важко тестувати окремо.

Відсутній return або await для async операцій:

typescript
// Неправильно: запит запускається, але відповідь повертається відразу @Get(':id') getUser(@Param('id') id: string) { this.usersService.findById(id); // немає return return { status: 'ok' }; // клієнт отримує неправильну відповідь } // Правильно @Get(':id') async getUser(@Param('id') id: string) { return this.usersService.findById(id); }

Повернення сирих об'єктів з бази даних:

typescript
// Неправильно: витікають passwordHash, internalNotes тощо @Get(':id') getUser(@Param('id') id: string) { return this.usersService.findById(id); } // Правильно: response DTO контролює що потрапить у відповідь @Get(':id') @UseInterceptors(ClassSerializerInterceptor) getUser(@Param('id') id: string): UserResponseDto { return this.usersService.findById(id); }

У більшості проектів саме через повернення сирих entity з контролера трапляються витоки даних. Response DTO дешево додати, і вони рятують від багатьох проблем.

Де зустрічається

  • REST API - кожен маршрут в NestJS додатку це метод контролера
  • GraphQL - resolvers замінюють контролери; @Resolver() грає ту саму роль
  • Мікросервіси - @MessagePattern() і @EventPattern() замінюють @Get()/@Post()
  • Fastify adapter - контролери працюють так само; NestJS абстрагує HTTP шар
  • AWS Lambda - кожен маршрут контролера може маппитися на окремий Lambda handler

Follow-up питання

Q: В чому різниця між контролером і сервісом?
A: Контролер відповідає за HTTP: маршрутизацію, статус-коди, форматування запиту і відповіді. Сервіс містить бізнес-логіку і може викликатися з будь-якого контексту. Контролер завжди викликає сервіс; сервіс ніколи не викликає контролер.

Q: Як обробляти помилки в контролері?
A: Кидай NestJS-виключення: BadRequestException, NotFoundException, ConflictException. NestJS автоматично перехоплює їх і повертає потрібний HTTP статус. Не давай сирим JavaScript помилкам потрапляти до клієнта.

Q: В чому різниця між @Body(), @Param() і @Query()?
A: @Body() читає тіло запиту (POST/PUT дані). @Param() читає path параметри як :id в /users/:id. @Query() читає значення з query string, наприклад ?page=1&limit=10.

Q: Як застосувати логування або авторизацію до всіх контролерів без дублювання коду?
A: Через interceptors і guards. Їх можна зареєструвати глобально в main.ts через app.useGlobalInterceptors() та app.useGlobalGuards(). Контролери залишаються чистими; спільні concerns живуть в одному місці.

Приклади

Базовий: CRUD контролер для ресурсу

typescript
import { Controller, Get, Post, Delete, Param, Body, Query, HttpCode, HttpStatus } from '@nestjs/common'; import { ProductsService } from './products.service'; import { CreateProductDto } from './dto/create-product.dto'; @Controller('api/products') export class ProductsController { constructor(private productsService: ProductsService) {} @Get() // GET /api/products?category=electronics&limit=10 listProducts( @Query('category') category?: string, @Query('limit') limit = 20 ) { return this.productsService.findAll({ category, limit }); // Повертає: { data: [...], total: 150 } } @Get(':id') // GET /api/products/42 getProduct(@Param('id') id: string) { return this.productsService.findById(parseInt(id)); // Повертає: { id: 42, name: 'Laptop', price: 999 } } @Post() @HttpCode(HttpStatus.CREATED) // POST /api/products → повертає 201 createProduct(@Body() dto: CreateProductDto) { return this.productsService.create(dto); // Повертає: { id: 43, name: 'Mouse', price: 25, createdAt: '...' } } @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) // DELETE /api/products/42 → повертає 204 без тіла deleteProduct(@Param('id') id: string) { this.productsService.delete(parseInt(id)); } }

@HttpCode(HttpStatus.CREATED) перевизначає стандартний статус 200. @HttpCode(HttpStatus.NO_CONTENT) каже NestJS повернути 204 без тіла відповіді. Обидва - HTTP concerns, яким саме тут місце.

Середній рівень: Обробник webhook із кастомним заголовком

typescript
import { Controller, Post, Body, Param, Headers, BadRequestException } from '@nestjs/common'; @Controller('api/orders') export class OrdersController { constructor(private ordersService: OrdersService) {} @Post(':id/webhook') // POST /api/orders/123/webhook async handleWebhook( @Param('id') orderId: string, @Body() payload: any, @Headers('x-webhook-signature') signature: string ) { // Перевіряємо підпис перед обробкою payload if (!this.verifySignature(payload, signature)) { throw new BadRequestException('Invalid webhook signature'); } await this.ordersService.processWebhook(parseInt(orderId), payload); return { status: 'processed', orderId: parseInt(orderId) }; } private verifySignature(payload: any, signature: string): boolean { return true; // логіка перевірки підпису тут } }

@Headers('x-webhook-signature') витягує конкретний заголовок із запиту. Контролер перевіряє підпис (HTTP concern), потім передає payload сервісу (бізнес-логіка). Чіткий розподіл між двома шарами.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?