Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке контролери в NestJS?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Controllers** (контролери) в NestJS - це класи, які приймають HTTP запити і повертають відповіді, використовуючи декоратори для прив'язки маршрутів до методів. ```typescript @Controller('users') export class UsersController { @Get(':id') // GET /users/123 getUser(@Param('id') id: string) { return this.usersService.findById(id); } } ``` **Головне:** контролери відповідають тільки за HTTP маршрутизацію; бізнес-логіка йде в сервіси.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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 сервісу (бізнес-логіка). Чіткий розподіл між двома шарами.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.