Тема 7. Фреймворк NestJS. Контролери та маршрутизація

HTTP-статус коди та декоратор @HttpCode

Встановлення кодів відповіді, стандартні коди за замовчуванням

HTTP-статус коди та декоратор @HttpCode

🎯 Мета лекції

  • Зрозуміти семантику HTTP-статус кодів та їх класифікацію за діапазонами
  • Опанувати стандартні коди за замовчуванням, які NestJS встановлює для різних HTTP-методів
  • Навчитися явно керувати статус кодами через декоратор @HttpCode
  • Вивчити використання enum-констант HttpStatus для читабельності коду
  • Засвоїти динамічне встановлення статусу через об'єкт Response у режимі passthrough
  • Практикувати правильний вибір статус кодів для різних сценаріїв REST API
  • Зрозуміти відмінність між 200 OK, 201 Created, 204 No Content та їх призначення
  • Навчитися обробляти асинхронні операції через 202 Accepted

🔑 Ключові терміни

  • HTTP Status Code (HTTP-статус код): тризначне число, що вказує результат обробки запиту
  • 2xx Success (успішні коди): запит оброблено успішно (200, 201, 204)
  • 3xx Redirection (перенаправлення): клієнт має виконати додаткову дію (301, 302, 304)
  • 4xx Client Error (помилки клієнта): запит містить синтаксичні або логічні помилки (400, 401, 404)
  • 5xx Server Error (помилки сервера): сервер не зміг обробити коректний запит (500, 502, 503)
  • Idempotency (ідемпотентність): властивість операції давати однаковий результат при повторах
  • Status Line (рядок статусу): перший рядок HTTP-відповіді з кодом та текстовим описом

HTTP-статус коди: мова протоколу HTTP

HTTP-статус коди є фундаментальним механізмом комунікації між клієнтом та сервером, що дозволяє серверу точно передати результат обробки запиту без необхідності парсингу тіла відповіді. Кожен HTTP-статус код є тризначним числом, де перша цифра визначає клас відповіді, а наступні дві — конкретний випадок всередині цього класу.

Статус код супроводжується текстовим описом (reason phrase), який надає людино-читабельне пояснення коду. Разом вони формують рядок статусу (status line) — перший рядок HTTP-відповіді:

HTTP/1.1 200 OK
HTTP/1.1 201 Created
HTTP/1.1 404 Not Found
HTTP/1.1 500 Internal Server Error

Розуміння семантики статус кодів є критично важливим для проєктування професійних RESTful API. Правильний вибір коду дозволяє клієнтським застосункам (браузерам, мобільним додаткам, інтеграціям) автоматично реагувати на різні ситуації без аналізу тіла відповіді. Наприклад, код 401 Unauthorized сигналізує про необхідність повторної автентифікації, 429 Too Many Requests — про перевищення ліміту запитів, а 503 Service Unavailable — про тимчасову недоступність сервісу.

Класифікація статус кодів за діапазонами

HTTP-статус коди поділяються на п'ять класів залежно від першої цифри:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

rectangle "1xx Informational" as INFO #E2E8F0 {
    note "Проміжні відповіді<br/>Рідко використовуються" as N1
    rectangle "100 Continue" as C100 #F1F5F9
    rectangle "101 Switching Protocols" as C101 #F1F5F9
}

rectangle "2xx Success" as SUCCESS #DCFCE7 {
    note "Запит успішно оброблено" as N2
    rectangle "200 OK" as C200 #22c55e
    rectangle "201 Created" as C201 #22c55e
    rectangle "202 Accepted" as C202 #22c55e
    rectangle "204 No Content" as C204 #22c55e
}

rectangle "3xx Redirection" as REDIRECT #DBEAFE {
    note "Клієнт має виконати додаткову дію" as N3
    rectangle "301 Moved Permanently" as C301 #3b82f6
    rectangle "302 Found" as C302 #3b82f6
    rectangle "304 Not Modified" as C304 #3b82f6
}

rectangle "4xx Client Error" as CLIENT_ERR #FED7AA {
    note "Помилка у запиті клієнта" as N4
    rectangle "400 Bad Request" as C400 #f59e0b
    rectangle "401 Unauthorized" as C401 #f59e0b
    rectangle "403 Forbidden" as C403 #f59e0b
    rectangle "404 Not Found" as C404 #f59e0b
    rectangle "422 Unprocessable Entity" as C422 #f59e0b
    rectangle "429 Too Many Requests" as C429 #f59e0b
}

rectangle "5xx Server Error" as SERVER_ERR #FEE2E2 {
    note "Помилка на стороні сервера" as N5
    rectangle "500 Internal Server Error" as C500 #ef4444
    rectangle "502 Bad Gateway" as C502 #ef4444
    rectangle "503 Service Unavailable" as C503 #ef4444
}

@enduml

1xx Informational (Інформаційні):
Проміжні відповіді, що сигналізують про прийняття запиту та продовження обробки. Використовуються рідко у типових REST API.

  • 100 Continue — сервер готовий прийняти тіло запиту (клієнт надіслав заголовки)
  • 101 Switching Protocols — сервер перемикається на інший протокол (наприклад, WebSocket)

2xx Success (Успішні):
Запит успішно отриманий, зрозумілий та оброблений сервером.

  • 200 OK — стандартна успішна відповідь (GET, PUT, PATCH)
  • 201 Created — ресурс успішно створено (POST)
  • 202 Accepted — запит прийнято до обробки, але обробка ще не завершена (асинхронні операції)
  • 204 No Content — успіх, але немає тіла відповіді (DELETE, PATCH без повернення даних)

3xx Redirection (Перенаправлення):
Клієнт має виконати додаткову дію для завершення запиту.

  • 301 Moved Permanently — ресурс остаточно переміщено на новий URL
  • 302 Found — тимчасове перенаправлення
  • 304 Not Modified — ресурс не змінювався з моменту останнього запиту (кешування)

4xx Client Error (Помилки клієнта):
Запит містить синтаксичні помилки або не може бути виконаний через некоректні дані.

  • 400 Bad Request — синтаксична помилка або некоректні дані
  • 401 Unauthorized — необхідна автентифікація
  • 403 Forbidden — доступ заборонено (навіть з автентифікацією)
  • 404 Not Found — ресурс не знайдено
  • 409 Conflict — конфлікт з поточним станом сервера (дублікати, версіонування)
  • 422 Unprocessable Entity — синтаксично коректні, але семантично невалідні дані
  • 429 Too Many Requests — перевищено ліміт запитів (rate limiting)

5xx Server Error (Помилки сервера):
Сервер зустрів помилку під час обробки коректного запиту.

  • 500 Internal Server Error — загальна помилка сервера
  • 502 Bad Gateway — помилка проксі-сервера або балансувальника
  • 503 Service Unavailable — сервіс тимчасово недоступний (технічні роботи, перевантаження)
Статус коди 4xx вказують на помилки, які клієнт може виправити (змінити дані, додати автентифікацію, змінити URL). Статус коди 5xx вказують на помилки, які клієнт не може виправити — потрібне втручання адміністратора сервера. Ця відмінність критична для автоматичної обробки помилок у клієнтських застосунках.

Семантика найпоширеніших кодів у REST API

Розглянемо детальніше коди, що використовуються у 95% RESTful API:

200 OK — Універсальний успіх:
Найпоширеніший код, що означає успішну обробку запиту. Використовується для GET (отримання), PUT (повне оновлення), PATCH (часткове оновлення).

GET /users/123 → 200 OK + тіло з даними користувача
PUT /users/123 → 200 OK + оновлені дані
PATCH /users/123 → 200 OK + оновлені дані

201 Created — Ресурс створено:
Використовується виключно для POST-запитів, що створюють новий ресурс. Часто супроводжується заголовком Location з URL новоствореного ресурсу.

POST /users → 201 Created
Location: /users/456
Body: { "id": 456, "name": "Alice", ... }

204 No Content — Успіх без тіла:
Операція виконана успішно, але немає даних для повернення. Типово використовується для DELETE або PATCH, коли клієнту не потрібна відповідь.

DELETE /users/123 → 204 No Content
(порожнє тіло відповіді)

400 Bad Request — Некоректний запит:
Клієнт надіслав синтаксично або семантично невалідні дані. Має супроводжуватися описом помилок у тілі відповіді.

POST /users
Body: { "email": "invalid-email" }
→ 400 Bad Request
Body: { "errors": ["email must be a valid email address"] }

401 Unauthorized — Необхідна автентифікація:
Запит вимагає автентифікації, але токен відсутній або невалідний. Має супроводжуватися заголовком WWW-Authenticate.

GET /profile (без заголовка Authorization)
→ 401 Unauthorized
WWW-Authenticate: Bearer realm="API"

404 Not Found — Ресурс не знайдено:
Запитаний ресурс не існує. Не слід плутати з 403 Forbidden (ресурс існує, але доступ заборонено).

GET /users/999 (користувача з ID 999 не існує)
→ 404 Not Found

409 Conflict — Конфлікт стану:
Запит не може бути виконаний через конфлікт з поточним станом сервера. Найчастіше використовується при спробі створити ресурс з унікальним полем, що вже існує (email, username, slug), або при оптимістичній блокуванні (версіонування даних).

POST /users
Body: { "email": "john@example.com" } (email вже існує у БД)
→ 409 Conflict
Body: { "error": "Email already exists", "field": "email" }

PUT /articles/123
Body: { "version": 5, ... } (поточна версія у БД = 6)
→ 409 Conflict
Body: { "error": "Version conflict. Resource was modified by another user." }
Відмінність 409 від 422: Обидва коди вказують на семантичну помилку, але з різним акцентом:
  • 409 Conflict — дані коректні, але конфліктують з існуючим станом (дублікат унікального поля, конфлікт версій, race condition)
  • 422 Unprocessable Entity — дані не відповідають бізнес-правилам домену (від'ємна ціна, недостатньо коштів, невалідна дата)
Якщо сумніваєтесь, використовуйте 422 як універсальний код для семантичних помилок. Код 409 резервуйте для очевидних конфліктів стану.

422 Unprocessable Entity — Семантична помилка:
Синтаксично коректний запит, але дані не можуть бути оброблені через порушення бізнес-правил. Відрізняється від 409 тим, що помилка не пов'язана з дублікатами або конфліктами стану, а з невідповідністю правилам домену.

POST /orders
Body: { "quantity": -5 } (від'ємна кількість неможлива)
→ 422 Unprocessable Entity
Body: { "error": "Quantity must be a positive number" }

POST /accounts/transfer
Body: { "amount": 10000 } (недостатньо коштів)
→ 422 Unprocessable Entity
Body: { "error": "Insufficient funds" }

500 Internal Server Error — Помилка сервера:
Неперехоплене виключення або непередбачена помилка на сервері. Має логуватися для аналізу розробниками.

GET /users/123 (виникла помилка БД)
→ 500 Internal Server Error
Body: { "error": "Internal Server Error" }
(деталі помилки НЕ повертаються клієнту з міркувань безпеки)

Стандартні статус коди за замовчуванням у NestJS

NestJS автоматично встановлює статус коди для HTTP-відповідей на основі HTTP-методу обробника. Ця поведінка відповідає найкращим практикам REST API та дозволяє розробникам не думати про статус коди у типових сценаріях.

Коди за замовчуванням для різних HTTP-методів

Loading diagram...
graph TB
    subgraph "NestJS Default Status Codes"
        GET["@Get()<br/>200 OK"]
        POST["@Post()<br/>201 Created"]
        PUT["@Put()<br/>200 OK"]
        PATCH["@Patch()<br/>200 OK"]
        DELETE["@Delete()<br/>200 OK"]
        HEAD["@Head()<br/>200 OK"]
        OPTIONS["@Options()<br/>200 OK"]
    end
    
    GET --> |"Повертає дані"| RETURN1["return {...}"]
    POST --> |"Створює ресурс"| RETURN2["return {...}"]
    PUT --> |"Оновлює повністю"| RETURN3["return {...}"]
    PATCH --> |"Оновлює частково"| RETURN4["return {...}"]
    DELETE --> |"Видаляє ресурс"| RETURN5["return {...}"]
    
    style POST fill:#22c55e,stroke:#15803d,color:#ffffff
    style GET fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style PUT fill:#f59e0b,stroke:#b45309,color:#ffffff
    style PATCH fill:#f59e0b,stroke:#b45309,color:#ffffff
    style DELETE fill:#ef4444,stroke:#b91c1c,color:#ffffff

Таблиця кодів за замовчуванням:

HTTP-методДекораторКод за замовчуваннямПояснення
GET@Get()200 OKОтримання ресурсу або колекції
POST@Post()201 CreatedСтворення нового ресурсу
PUT@Put()200 OKПовна заміна існуючого ресурсу
PATCH@Patch()200 OKЧасткове оновлення існуючого ресурсу
DELETE@Delete()200 OKВидалення ресурсу
HEAD@Head()200 OKОтримання заголовків без тіла
OPTIONS@Options()200 OKІнформація про підтримувані методи

Приклади поведінки за замовчуванням

GET-запит — 200 OK:

import { Controller, Get } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Get()
  findAll() {
    // NestJS автоматично встановлює статус 200 OK
    return [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' },
    ];
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    // Також 200 OK
    return { id, name: 'Alice', email: 'alice@example.com' };
  }
}

Відповідь сервера:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 65

[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]

POST-запит — 201 Created:

@Controller('users')
export class UsersController {
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    const newUser = this.usersService.create(createUserDto);
    
    // NestJS автоматично встановлює статус 201 Created
    return newUser;
  }
}

Відповідь сервера:

HTTP/1.1 201 Created
Content-Type: application/json
Location: /users/456

{"id":456,"name":"Alice","email":"alice@example.com"}
NestJS встановлює статус 201 Created для POST-запитів автоматично, відповідаючи семантиці REST. Це єдиний HTTP-метод, для якого код за замовчуванням відрізняється від 200 OK, що підкреслює особливу роль POST як операції створення ресурсу.

PUT/PATCH-запити — 200 OK:

@Controller('users')
export class UsersController {
  @Put(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    const updatedUser = this.usersService.update(id, updateUserDto);
    
    // Статус 200 OK за замовчуванням
    return updatedUser;
  }

  @Patch(':id')
  partialUpdate(@Param('id') id: string, @Body() patchDto: Partial<UpdateUserDto>) {
    const updatedUser = this.usersService.partialUpdate(id, patchDto);
    
    // Також 200 OK за замовчуванням
    return updatedUser;
  }
}

DELETE-запит — 200 OK:

@Controller('users')
export class UsersController {
  @Delete(':id')
  remove(@Param('id') id: string) {
    this.usersService.remove(id);
    
    // Статус 200 OK за замовчуванням
    return { message: 'User deleted successfully' };
  }
}
Хоча NestJS встановлює 200 OK для DELETE за замовчуванням, у багатьох REST API практикується використання 204 No Content для DELETE, що сигналізує про успішне видалення без повернення тіла відповіді. Для цього потрібно явно встановити статус через декоратор @HttpCode(204).

Переваги поведінки за замовчуванням

Автоматичне встановлення статус кодів NestJS має кілька переваг:

Менше boilerplate коду:

// ❌ Без NestJS: Express (явне встановлення статусу)
app.post('/users', (req, res) => {
  const user = createUser(req.body);
  res.status(201).json(user); // Ручне встановлення 201
});

// ✅ З NestJS: автоматичний 201 для POST
@Post()
create(@Body() dto: CreateUserDto) {
  return this.usersService.create(dto);
  // Статус 201 встановлюється автоматично
}

Відповідність REST-конвенціям:
NestJS слідує стандартам REST API, встановлюючи правильні статус коди для кожного HTTP-методу без додаткових зусиль розробника.

Консистентність кодової бази:
Всі обробники у проєкті автоматично повертають правильні статус коди, що зменшує ймовірність помилок та підвищує передбачуваність API.

Фокус на бізнес-логіці:
Розробники можуть зосередитися на бізнес-логіці, не турбуючись про низькорівневі деталі протоколу HTTP.

Поведінка за замовчуванням покриває типові сценарії REST API. У специфічних випадках (асинхронна обробка, кастомні операції, помилки) потрібне явне встановлення статус коду через декоратор @HttpCode() або об'єкт Response.

Декоратор @HttpCode: явне керування статус кодом

Коли поведінка за замовчуванням не відповідає вашим потребам, NestJS надає декоратор @HttpCode() для явного встановлення статус коду відповіді. Цей декоратор застосовується на рівні методу контролера та перевизначає код за замовчуванням.

Базовий синтаксис

import { Controller, Delete, HttpCode } from '@nestjs/common';

@Controller('users')
export class UsersController {
  @Delete(':id')
  @HttpCode(204)  // Явно встановлюємо 204 No Content
  remove(@Param('id') id: string) {
    this.usersService.remove(id);
    // Тіло відповіді можна не повертати (204 означає "немає тіла")
  }
}

Відповідь сервера:

HTTP/1.1 204 No Content
Content-Length: 0

(порожнє тіло)

Використання з enum HttpStatus

Для покращення читабельності та уникнення магічних чисел NestJS експортує enum HttpStatus з усіма стандартними кодами:

import { Controller, Post, HttpCode, HttpStatus } from '@nestjs/common';

@Controller('orders')
export class OrdersController {
  @Post()
  @HttpCode(HttpStatus.ACCEPTED) // 202 Accepted
  createOrder(@Body() createOrderDto: CreateOrderDto) {
    // Асинхронна обробка замовлення
    this.ordersService.queueOrder(createOrderDto);
    
    return {
      message: 'Order accepted for processing',
      estimatedTime: '5 minutes',
    };
  }
}

Найпоширеніші константи HttpStatus:

HttpStatus.OK                    // 200
HttpStatus.CREATED               // 201
HttpStatus.ACCEPTED              // 202
HttpStatus.NO_CONTENT            // 204

HttpStatus.MOVED_PERMANENTLY     // 301
HttpStatus.FOUND                 // 302
HttpStatus.NOT_MODIFIED          // 304

HttpStatus.BAD_REQUEST           // 400
HttpStatus.UNAUTHORIZED          // 401
HttpStatus.FORBIDDEN             // 403
HttpStatus.NOT_FOUND             // 404
HttpStatus.UNPROCESSABLE_ENTITY  // 422
HttpStatus.TOO_MANY_REQUESTS     // 429

HttpStatus.INTERNAL_SERVER_ERROR // 500
HttpStatus.BAD_GATEWAY           // 502
HttpStatus.SERVICE_UNAVAILABLE   // 503
Завжди використовуйте HttpStatus замість магічних чисел. Це підвищує читабельність коду та зменшує ймовірність помилок:
// ❌ Погано: магічне число
@HttpCode(204)

// ✅ Добре: іменована константа
@HttpCode(HttpStatus.NO_CONTENT)

Приклад: 204 No Content для DELETE

У REST API операція видалення зазвичай не повертає тіло відповіді, використовуючи статус 204 No Content:

import { Controller, Delete, Param, HttpCode, HttpStatus } from '@nestjs/common';

@Controller('articles')
export class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  async remove(@Param('id') id: string): Promise<void> {
    await this.articlesService.remove(id);
    
    // Немає return — 204 означає порожнє тіло
  }
}
DELETE запит з 204 No Content
$ curl -X DELETE http://localhost:3000/articles/123 -v
DELETE /articles/123 HTTP/1.1
Host: localhost:3000
HTTP/1.1 204 No Content
Date: Fri, 04 Sep 2026 14:30:00 GMT
Connection: keep-alive
(порожнє тіло відповіді)
Якщо метод повертає void або undefined, NestJS не надсилає тіло відповіді, що ідеально підходить для статусу 204. Якщо ви випадково повернете дані, вони не потраплять у відповідь при статусі 204.

Приклад: 202 Accepted для асинхронної обробки

Коли операція приймається до обробки, але виконується асинхронно (через черги, фонові задачі), використовується статус 202 Accepted:

import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';

@Controller('reports')
export class ReportsController {
  constructor(
    private readonly reportsService: ReportsService,
    private readonly queueService: QueueService,
  ) {}

  @Post('generate')
  @HttpCode(HttpStatus.ACCEPTED)
  async generateReport(@Body() reportDto: GenerateReportDto) {
    // Додаємо завдання у чергу для асинхронної обробки
    const jobId = await this.queueService.addJob('generate-report', reportDto);
    
    return {
      message: 'Report generation accepted',
      jobId,
      status: 'pending',
      estimatedCompletion: '10 minutes',
      checkStatusUrl: `/reports/status/${jobId}`,
    };
  }

  @Get('status/:jobId')
  async checkStatus(@Param('jobId') jobId: string) {
    const status = await this.queueService.getJobStatus(jobId);
    
    return {
      jobId,
      status: status.state, // pending, processing, completed, failed
      progress: status.progress,
      result: status.result,
    };
  }
}
Асинхронна обробка з 202 Accepted
$ curl -X POST http://localhost:3000/reports/generate -H "Content-Type: application/json" \
-d '{"type":"monthly","year":2026,"month":8}'
HTTP/1.1 202 Accepted
{
"message": "Report generation accepted",
"jobId": "job-abc-123",
"status": "pending",
"estimatedCompletion": "10 minutes",
"checkStatusUrl": "/reports/status/job-abc-123"
}
# Перевірка статусу через 5 хвилин
$ curl http://localhost:3000/reports/status/job-abc-123
HTTP/1.1 200 OK
{
"jobId": "job-abc-123",
"status": "completed",
"progress": 100,
"result": {
"fileUrl": "/downloads/reports/monthly-2026-08.pdf"
}
}

Приклад: 200 OK замість 201 для ідемпотентних POST

Іноді POST-запити є ідемпотентними (idempotent) — повторний запит з тими самими даними не створює новий ресурс, а повертає існуючий. У таких випадках логічніше повертати 200 OK:

import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';

@Controller('subscriptions')
export class SubscriptionsController {
  constructor(private readonly subscriptionsService: SubscriptionsService) {}

  @Post('subscribe')
  @HttpCode(HttpStatus.OK) // Замість 201 Created
  async subscribe(@Body() subscribeDto: SubscribeDto) {
    // Якщо підписка вже існує, не створюємо нову
    const subscription = await this.subscriptionsService.findOrCreate(subscribeDto);
    
    return {
      message: subscription.isNew ? 'Subscribed successfully' : 'Already subscribed',
      subscription,
      isNew: subscription.isNew,
    };
  }
}

Динамічна зміна статусу через @Res

Декоратор @HttpCode() встановлює статичний статус код, який застосовується до всіх запитів до даного обробника. Проте іноді потрібно динамічно визначити статус на основі результату бізнес-логіки. Для цього використовується об'єкт Response у режимі passthrough.

Режим passthrough: гібридний підхід

Як ми вивчали у попередній лекції про об'єкти Request та Response, режим passthrough: true дозволяє модифікувати відповідь, зберігаючи автоматичну серіалізацію NestJS:

import { Controller, Get, Param, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cache')
export class CacheController {
  constructor(private readonly cacheService: CacheService) {}

  @Get('resource/:id')
  async getResource(
    @Param('id') id: string,
    @Res({ passthrough: true }) res: Response,
  ) {
    const resource = await this.cacheService.findById(id);
    
    if (!resource) {
      // Динамічно встановлюємо 404
      res.status(HttpStatus.NOT_FOUND);
      return { error: 'Resource not found' };
    }
    
    if (!resource.isModified) {
      // Динамічно встановлюємо 304 Not Modified
      res.status(HttpStatus.NOT_MODIFIED);
      return; // Порожнє тіло для 304
    }
    
    // За замовчуванням 200 OK
    return resource;
  }
}

Приклад: Умовне повернення 304 Not Modified

Статус 304 Not Modified використовується для кешування — якщо ресурс не змінювався з моменту останнього запиту, сервер повертає 304 без тіла, заощаджуючи трафік:

import { Controller, Get, Param, Headers, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('articles')
export class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}

  @Get(':id')
  async findOne(
    @Param('id') id: string,
    @Headers('if-none-match') ifNoneMatch: string,
    @Res({ passthrough: true }) res: Response,
  ) {
    const article = await this.articlesService.findById(id);
    
    if (!article) {
      res.status(HttpStatus.NOT_FOUND);
      return { error: 'Article not found' };
    }

    // Генеруємо ETag на основі версії ресурсу
    const etag = `"${article.version}"`;
    res.header('ETag', etag);

    // Клієнт має актуальну версію?
    if (ifNoneMatch === etag) {
      res.status(HttpStatus.NOT_MODIFIED);
      return; // Порожнє тіло — клієнт використає кешовану версію
    }

    // Ресурс змінився — повертаємо повні дані
    res.header('Cache-Control', 'private, must-revalidate');
    return article;
  }
}
Умовне кешування через ETag
# Перший запит — отримуємо повні дані
$ curl -v http://localhost:3000/articles/123
HTTP/1.1 200 OK
ETag: "v42"
Cache-Control: private, must-revalidate
{
"id": 123,
"title": "Understanding HTTP Caching",
"version": 42
}
# Повторний запит з ETag — отримуємо 304
$ curl -v http://localhost:3000/articles/123 -H "If-None-Match: \"v42\""
HTTP/1.1 304 Not Modified
ETag: "v42"
(порожнє тіло — клієнт використовує кешовану версію)

Приклад: Асинхронні операції з різними статусами

У складних сценаріях одна й та сама операція може повертати різні статус коди залежно від стану системи:

import { Controller, Post, Body, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('payments')
export class PaymentsController {
  constructor(
    private readonly paymentsService: PaymentsService,
    private readonly queueService: QueueService,
  ) {}

  @Post('process')
  async processPayment(
    @Body() paymentDto: ProcessPaymentDto,
    @Res({ passthrough: true }) res: Response,
  ) {
    const systemLoad = await this.queueService.getCurrentLoad();

    if (systemLoad > 0.8) {
      // Система перевантажена — приймаємо до асинхронної обробки
      const jobId = await this.queueService.addJob('payment', paymentDto);
      
      res.status(HttpStatus.ACCEPTED); // 202 Accepted
      return {
        message: 'Payment accepted for processing',
        jobId,
        checkStatusUrl: `/payments/status/${jobId}`,
      };
    }

    // Система має ресурси — обробляємо синхронно
    const result = await this.paymentsService.process(paymentDto);

    if (result.requiresConfirmation) {
      // Потрібне підтвердження 3D-Secure
      res.status(HttpStatus.OK); // 200 OK
      return {
        message: 'Confirmation required',
        confirmationUrl: result.confirmationUrl,
      };
    }

    // Платіж успішно виконано
    res.status(HttpStatus.CREATED); // 201 Created
    return {
      message: 'Payment processed successfully',
      transactionId: result.transactionId,
      amount: result.amount,
    };
  }
}
Уникайте надмірного використання динамічних статусів! У 90% випадків статичного коду через @HttpCode() достатньо. Динамічні статуси виправдані лише у:
  • Умовному кешуванні (304 Not Modified)
  • Асинхронних операціях з різною швидкістю обробки
  • Складних бізнес-процесах з кількома варіантами результату
Для стандартних помилок (404, 400, 401) використовуйте виключення (Exceptions), які ми розглянемо у наступних лекціях.

Best Practices: правильний вибір статус кодів

Вибір правильного HTTP-статус коду є мистецтвом балансу між дотриманням стандартів, зручністю для клієнтів та семантичною точністю. Розглянемо найкращі практики для різних сценаріїв REST API.

Правила вибору статус коду

Крок 1. Визначте категорію результату

Спершу визначте, чи операція успішна (2xx), чи відбулася помилка (4xx/5xx):

  • Запит оброблено успішно → 2xx
  • Помилка у запиті клієнта → 4xx
  • Помилка на сервері → 5xx
  • Потрібне перенаправлення → 3xx

Крок 2. Уточніть тип успіху (для 2xx)

Якщо операція успішна, визначте конкретний код:

  • Отримання даних → 200 OK
  • Створення ресурсу → 201 Created
  • Асинхронна обробка → 202 Accepted
  • Успіх без даних → 204 No Content

Крок 3. Уточніть тип помилки (для 4xx)

Якщо помилка від клієнта, визначте причину:

  • Синтаксична помилка → 400 Bad Request
  • Немає автентифікації → 401 Unauthorized
  • Доступ заборонено → 403 Forbidden
  • Ресурс не знайдено → 404 Not Found
  • Конфлікт даних → 409 Conflict
  • Семантична помилка → 422 Unprocessable Entity
  • Перевищено ліміт → 429 Too Many Requests

Крок 4. Додайте деталі у тіло відповіді

Завжди супроводжуйте помилки 4xx/5xx детальним описом у тілі відповіді:

{
  "statusCode": 400,
  "message": "Validation failed",
  "errors": [
    "email must be a valid email address",
    "password must be at least 8 characters long"
  ]
}

Матриця вибору статус коду для CRUD-операцій

ОпераціяМетодУспіхРесурс не знайденоВалідаціяКонфлікт
СписокGET200 OK200 OK (порожній масив)——
ДеталіGET200 OK404 Not Found——
СтворенняPOST201 Created—400/422409 Conflict
Повне оновленняPUT200 OK404 Not Found400/422409 Conflict
Часткове оновленняPATCH200 OK404 Not Found400/422—
ВидаленняDELETE204 No Content404 Not Found*——

* Деякі API повертають 204 No Content навіть якщо ресурс не існує (ідемпотентність DELETE).

Приклади правильного використання

@Controller('users')
export class UsersController {
  @Get()
  async findAll(@Query() filterDto: FilterUsersDto) {
    const users = await this.usersService.findAll(filterDto);
    
    // ✅ Завжди 200 OK, навіть якщо масив порожній
    return {
      data: users,
      total: users.length,
      page: filterDto.page || 1,
    };
  }
}

Типові помилки при виборі статус кодів

Таблиця статус кодів для типових сценаріїв

СценарійСтатус кодКоли використовувати
Успішне отримання даних200 OKGET запити, що повертають дані
Успішне створення201 CreatedPOST запити, що створюють ресурс
Асинхронна обробка202 AcceptedОперації, що виконуються у фоні
Успіх без даних204 No ContentDELETE, PATCH без повернення даних
Ресурс не змінювався304 Not ModifiedУмовне кешування через ETag
Синтаксична помилка400 Bad RequestНекоректний JSON, відсутні поля
Немає автентифікації401 UnauthorizedВідсутній або невалідний токен
Доступ заборонено403 ForbiddenНедостатньо прав доступу
Ресурс не знайдено404 Not FoundGET/PUT/PATCH/DELETE неіснуючого ресурсу
Конфлікт даних409 ConflictДублікат унікального поля
Семантична помилка422 Unprocessable EntityПорушення бізнес-правил
Перевищено ліміт429 Too Many RequestsRate limiting
Помилка сервера500 Internal Server ErrorНепередбачені помилки
Сервіс недоступний503 Service UnavailableТехнічні роботи, БД недоступна
Золоте правило: Якщо сумніваєтесь у виборі коду, використовуйте найпростіший:
  • Успіх → 200 OK
  • Помилка клієнта → 400 Bad Request
  • Помилка сервера → 500 Internal Server Error
Уточнення кодів (201, 204, 401, 404, 422) додавайте по мірі дозрівання API та потреб клієнтів.

Практичний приклад: RESTful CRUD з правильними кодами

Розглянемо повний приклад контролера, що демонструє правильне використання HTTP-статус кодів для всіх CRUD-операцій:

import {
  Controller,
  Get,
  Post,
  Put,
  Patch,
  Delete,
  Param,
  Body,
  Query,
  HttpCode,
  HttpStatus,
  NotFoundException,
  ConflictException,
} from '@nestjs/common';
import { ArticlesService } from './articles.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { FilterArticlesDto } from './dto/filter-articles.dto';

@Controller('articles')
export class ArticlesController {
  constructor(private readonly articlesService: ArticlesService) {}

  // GET /articles — Отримання списку статей
  @Get()
  async findAll(@Query() filterDto: FilterArticlesDto) {
    const articles = await this.articlesService.findAll(filterDto);
    
    // ✅ 200 OK (за замовчуванням)
    // Навіть якщо масив порожній — це не помилка!
    return {
      data: articles,
      total: articles.length,
      page: filterDto.page || 1,
      limit: filterDto.limit || 10,
    };
  }

  // GET /articles/:id — Отримання однієї статті
  @Get(':id')
  async findOne(@Param('id') id: string) {
    const article = await this.articlesService.findById(id);
    
    if (!article) {
      // ✅ 404 Not Found якщо ресурс не існує
      throw new NotFoundException(`Article with ID ${id} not found`);
    }
    
    // ✅ 200 OK (за замовчуванням)
    return article;
  }

  // POST /articles — Створення нової статті
  @Post()
  // ✅ 201 Created встановлюється автоматично для POST
  // Але можна явно вказати для наочності:
  @HttpCode(HttpStatus.CREATED)
  async create(@Body() createArticleDto: CreateArticleDto) {
    // Перевірка унікальності slug
    const existing = await this.articlesService.findBySlug(createArticleDto.slug);
    
    if (existing) {
      // ✅ 409 Conflict для дублікату унікального поля
      throw new ConflictException(`Article with slug "${createArticleDto.slug}" already exists`);
    }
    
    const article = await this.articlesService.create(createArticleDto);
    
    // ✅ Повертаємо створений ресурс з ID
    return article;
  }

  // PUT /articles/:id — Повна заміна статті
  @Put(':id')
  async replace(
    @Param('id') id: string,
    @Body() updateArticleDto: UpdateArticleDto,
  ) {
    const article = await this.articlesService.findById(id);
    
    if (!article) {
      // ✅ 404 Not Found для неіснуючого ресурсу
      throw new NotFoundException(`Article with ID ${id} not found`);
    }
    
    const updated = await this.articlesService.replace(id, updateArticleDto);
    
    // ✅ 200 OK (за замовчуванням) з оновленими даними
    return updated;
  }

  // PATCH /articles/:id — Часткове оновлення статті
  @Patch(':id')
  async update(
    @Param('id') id: string,
    @Body() patchDto: Partial<UpdateArticleDto>,
  ) {
    const article = await this.articlesService.findById(id);
    
    if (!article) {
      // ✅ 404 Not Found для неіснуючого ресурсу
      throw new NotFoundException(`Article with ID ${id} not found`);
    }
    
    const updated = await this.articlesService.update(id, patchDto);
    
    // ✅ 200 OK (за замовчуванням) з оновленими даними
    return updated;
  }

  // DELETE /articles/:id — Видалення статті
  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT) // ✅ 204 No Content
  async remove(@Param('id') id: string): Promise<void> {
    const article = await this.articlesService.findById(id);
    
    if (!article) {
      // ✅ 404 Not Found для неіснуючого ресурсу
      throw new NotFoundException(`Article with ID ${id} not found`);
    }
    
    await this.articlesService.remove(id);
    
    // ✅ Немає return — порожнє тіло для 204
  }

  // POST /articles/:id/publish — Спеціальна операція (публікація)
  @Post(':id/publish')
  @HttpCode(HttpStatus.OK) // ✅ 200 OK замість 201 (не створюємо ресурс)
  async publish(@Param('id') id: string) {
    const article = await this.articlesService.findById(id);
    
    if (!article) {
      throw new NotFoundException(`Article with ID ${id} not found`);
    }
    
    if (article.published) {
      // ✅ 409 Conflict якщо вже опубліковано
      throw new ConflictException('Article is already published');
    }
    
    const published = await this.articlesService.publish(id);
    
    return {
      message: 'Article published successfully',
      article: published,
    };
  }
}
Взаємодія з RESTful API
# Отримання списку статей
$ curl http://localhost:3000/articles
HTTP/1.1 200 OK
{ "data": [...], "total": 5 }
# Отримання однієї статті
$ curl http://localhost:3000/articles/123
HTTP/1.1 200 OK
{ "id": 123, "title": "..." }
# Створення нової статті
$ curl -X POST http://localhost:3000/articles -d '{"title":"..."}'
HTTP/1.1 201 Created
{ "id": 456, "title": "..." }
# Спроба створити дублікат
$ curl -X POST http://localhost:3000/articles -d '{"slug":"existing-slug"}'
HTTP/1.1 409 Conflict
{ "message": "Article with slug \"existing-slug\" already exists" }
# Часткове оновлення статті
$ curl -X PATCH http://localhost:3000/articles/123 -d '{"title":"Updated"}'
HTTP/1.1 200 OK
{ "id": 123, "title": "Updated" }
# Видалення статті
$ curl -X DELETE http://localhost:3000/articles/123
HTTP/1.1 204 No Content
(порожнє тіло)
# Спроба отримати видалену статтю
$ curl http://localhost:3000/articles/123
HTTP/1.1 404 Not Found
{ "message": "Article with ID 123 not found" }

Порівняння підходів: @HttpCode vs @Res vs Exceptions

NestJS надає три механізми для керування HTTP-статус кодами. Розглянемо їх порівняння та сценарії використання:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

rectangle M1 #DCFCE7 [
<b>Механізм 1: @HttpCode</b>
--
Статичний код для успішних операцій
@HttpCode(HttpStatus.NO_CONTENT)
@Delete(':id')
async remove(...) { }
]

rectangle M2 #DBEAFE [
<b>Механізм 2: @Res({ passthrough })</b>
--
Динамічний код на основі логіки
@Get(':id')
async find(..., @Res({ passthrough }) res) {
  if (condition) res.status(304);
  return data;
}
]

rectangle M3 #FED7AA [
<b>Механізм 3: Exceptions</b>
--
Автоматичний код для помилок
@Get(':id')
async find(...) {
  if (!found) {
    throw new NotFoundException();
  }
}
]

rectangle SUCCESS #DCFCE7 [
2xx коди
]

rectangle ERROR #FCA5A5 [
4xx/5xx коди
]

M1 --> SUCCESS : Успіх
M2 --> SUCCESS : Успіх (умовний)
M3 --> ERROR : Помилки
@enduml

Порівняльна таблиця

Аспект@HttpCode@Res({ passthrough })Exceptions
Коли використовуватиСтатичний код для успіхуДинамічний код на основі логікиПомилки 4xx/5xx
СкладністьНайпростішийСередняНайпростіший
ГнучкістьНизька (статичний)Висока (динамічний)Середня
ЧитабельністьВідміннаДобраВідмінна
ТестуванняЛегкоСкладнішеЛегко
Приклади201 Created, 204 No Content304 Not Modified, 202 Accepted404 Not Found, 400 Bad Request

Рекомендації щодо вибору підходу

Крок 1. Статичні успішні коди — @HttpCode

Використовуйте @HttpCode() для передбачуваних успішних операцій з фіксованим статус кодом:

// ✅ ДОБРЕ: статичний код для DELETE
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string): Promise<void> {
  await this.service.remove(id);
}

// ✅ ДОБРЕ: статичний код для асинхронної операції
@Post('export')
@HttpCode(HttpStatus.ACCEPTED)
async export(@Body() dto: ExportDto) {
  const jobId = await this.queueService.addJob('export', dto);
  return { jobId, status: 'pending' };
}

Крок 2. Динамічні успішні коди — @Res

Використовуйте @Res({ passthrough: true }) для умовних кодів на основі бізнес-логіки:

// ✅ ДОБРЕ: динамічний код для кешування
@Get(':id')
async find(
  @Param('id') id: string,
  @Headers('if-none-match') etag: string,
  @Res({ passthrough: true }) res: Response,
) {
  const resource = await this.service.findById(id);
  
  if (etag === resource.etag) {
    res.status(HttpStatus.NOT_MODIFIED);
    return;
  }
  
  res.header('ETag', resource.etag);
  return resource;
}

Крок 3. Помилки — Exceptions

Використовуйте виключення для всіх помилок 4xx/5xx:

// ✅ ДОБРЕ: виключення для помилок
@Get(':id')
async findOne(@Param('id') id: string) {
  const resource = await this.service.findById(id);
  
  if (!resource) {
    throw new NotFoundException(`Resource ${id} not found`);
    // → 404 Not Found
  }
  
  return resource;
}

@Post()
async create(@Body() dto: CreateDto) {
  const existing = await this.service.findByEmail(dto.email);
  
  if (existing) {
    throw new ConflictException('Email already exists');
    // → 409 Conflict
  }
  
  return this.service.create(dto);
}
Уникайте змішування підходів! Не встановлюйте статус через @Res для помилок — використовуйте виключення. Не використовуйте виключення для успішних нестандартних кодів (202, 204) — використовуйте @HttpCode().

Підсумки та ключові висновки

✅ Ключові моменти

  • HTTP-статус коди — це стандартизована мова протоколу HTTP для передачі результату обробки запиту
  • NestJS автоматично встановлює правильні коди: 200 OK для GET/PUT/PATCH/DELETE, 201 Created для POST
  • Декоратор @HttpCode() дозволяє явно встановити статичний код для методу
  • Enum HttpStatus покращує читабельність коду та запобігає магічним числам
  • Для динамічних кодів використовуйте @Res({ passthrough: true })
  • Для помилок 4xx/5xx використовуйте виключення (Exceptions)
  • 204 No Content рекомендується для DELETE без повернення даних
  • 202 Accepted використовується для асинхронних операцій
  • 409 Conflict сигналізує про конфлікт унікальних полів (не 400!)
  • 401 Unauthorized — немає автентифікації, 403 Forbidden — недостатньо прав

🎓 Практичні рекомендації

  • У 95% випадків покладайтеся на автоматичні коди NestJS
  • Додавайте @HttpCode() лише для специфічних операцій (DELETE → 204, асинхронність → 202)
  • Завжди використовуйте HttpStatus замість чисел: HttpStatus.NO_CONTENT замість 204
  • Супроводжуйте помилки детальним описом у тілі відповіді
  • Дотримуйтеся семантики REST: 2xx — успіх, 4xx — клієнт, 5xx — сервер
  • Тестуйте API з різними статус кодами для перевірки поведінки клієнтів
  • Документуйте очікувані статус коди у Swagger через @ApiResponse()

Подальше вивчення

У наступних лекціях ми розглянемо:

  • Перенаправлення та декоратор @Redirect — робота з 3xx кодами
  • Асинхронні обробники та Promise — async/await у контролерах
  • Обробка помилок та Exceptions — детальна робота з 4xx/5xx
  • Guards та автентифікація — автоматичне встановлення 401/403
  • Interceptors — глобальна трансформація відповідей та кодів
Практикуйте правильний вибір статус кодів на реальних проєктах. Спробуйте переглянути відомі публічні API (GitHub, Stripe, Twitter) через інструменти розробника браузера та проаналізуйте, які коди вони повертають для різних сценаріїв. Це найкращий спосіб засвоїти REST-конвенції.
Copyright © 2026