Що таке контролери в NestJS?
Controllers (контролери) в NestJS - це класи, які приймають HTTP запити і повертають відповіді. Декоратори прив'язують URL-шляхи та HTTP методи до конкретних функцій-обробників.
Теорія
TL;DR
- Контролер - точка входу для HTTP: отримує запит, витягує дані, викликає сервіс, повертає відповідь
@Controller('users')задає префікс маршруту;@Get(),@Post()прив'язують HTTP методи до методів класу- Контролер відповідає за HTTP (маршрути, статус-коди, заголовки); бізнес-логіка йде в сервіси
- Правило: якщо це HTTP - в контролер; якщо бізнес-логіка - в сервіс
Швидкий приклад
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() дає прямий контроль над об'єктом відповіді.
Поширені помилки
Бізнес-логіка в контролері:
// Неправильно: контролер звертається до бази даних і обчислює поля
@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 операцій:
// Неправильно: запит запускається, але відповідь повертається відразу
@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);
}Повернення сирих об'єктів з бази даних:
// Неправильно: витікають 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 контролер для ресурсу
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 із кастомним заголовком
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 сервісу (бізнес-логіка). Чіткий розподіл між двома шарами.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.