Пошук уроків, статей та іншого контенту
Розберете послідовність проходження запиту через middleware, guards, interceptors, pipes, контролер і filters.
Коли клієнт надсилає HTTP-запит до NestJS, він проходить через кілька етапів. Кожен етап відповідає за окрему задачу:
Middleware — виконує підготовчу роботу для запиту.
Guards — вирішують, чи дозволено продовжувати обробку.
Interceptors — виконують код до та після обробника маршруту.
Pipes — перевіряють і перетворюють вхідні дані.
Контролер — обробляє маршрут і повертає результат.
Filters — перехоплюють необроблені помилки та формують відповідь про помилку.
У спрощеному вигляді послідовність має такий вигляд:
Запит
↓
Middleware
↓
Guards
↓
Interceptors: частина до обробника
↓
Pipes
↓
Контролер і сервіс
↓
Interceptors: частина після обробника
↓
ВідповідьЯкщо на будь-якому етапі виникає необроблена помилка, керування переходить до exception filter.
Middleware — це функція або клас, який отримує об’єкти запиту та відповіді й може передати керування далі за допомогою next().
Middleware часто використовують для:
журналювання запитів;
додавання даних до об’єкта запиту;
перевірки простих заголовків;
виконання підготовчих дій перед обробкою маршруту.
Middleware виконується до guards.
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class RequestLogMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[Middleware] ${req.method} ${req.originalUrl}`);
// Передаємо керування наступному етапу
next();
}
}Якщо middleware не викличе next() і не завершить відповідь самостійно, запит зупиниться.
Middleware підключається в модулі:
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestLogMiddleware).forRoutes('*');
}
}У цьому прикладі middleware застосовується до всіх маршрутів.
Guard реалізує інтерфейс CanActivate і повертає:
true, якщо запит може продовжити обробку;
false, якщо доступ заборонено;
або може викинути виняток.
Guards зазвичай використовують для авторизації та перевірки доступу.
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
// Перевіряємо значення заголовка
return request.headers['x-api-key'] === 'secret-key';
}
}Якщо guard поверне false, NestJS зазвичай відповість статусом 403 Forbidden, а наступні етапи для цього запиту виконуватися не будуть.
Guard може отримати інформацію про маршрут через ExecutionContext, наприклад параметри запиту або метадані обробника.
Interceptor обгортає виконання обробника маршруту. Він може виконати код:
перед викликом контролера;
після успішного виконання контролера;
під час обробки помилки;
для перетворення результату.
Основний метод interceptor — intercept().
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
@Injectable()
export class TimingInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const startedAt = Date.now();
console.log('[Interceptor] До контролера');
return next.handle().pipe(
finalize(() => {
const duration = Date.now() - startedAt;
console.log(`[Interceptor] Запит завершено за ${duration} мс`);
}),
);
}
}next.handle() запускає наступний етап і повертає Observable з результатом обробника.
Interceptor може змінити результат за допомогою операторів RxJS. Наприклад, можна обгорнути результат у властивість data, але в цьому прикладі він лише вимірює час виконання.
Pipe отримує вхідне значення та може:
перевірити його;
перетворити його;
викинути помилку, якщо значення некоректне.
Pipes виконуються перед викликом методу контролера.
Розглянемо pipe, який перетворює параметр маршруту на додатне ціле число:
import {
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class PositiveIntegerPipe
implements PipeTransform<string, number>
{
transform(value: string): number {
const numberValue = Number(value);
if (!Number.isInteger(numberValue) || numberValue <= 0) {
throw new BadRequestException(
'Параметр id має бути додатним цілим числом',
);
}
return numberValue;
}
}Pipe можна застосувати безпосередньо до параметра контролера:
@Get(':id')
findOne(
@Param('id', PositiveIntegerPipe) id: number,
) {
return { id };
}У такому разі значення id, яке спочатку є рядком у URL, потрапить до контролера вже як число.
NestJS також має вбудовані pipes, наприклад ParseIntPipe:
@Get(':id')
findOne(
@Param('id', ParseIntPipe) id: number,
) {
return { id };
}Після успішного проходження guards, interceptors і pipes NestJS викликає метод контролера.
Контролер відповідає за HTTP-рівень:
приймає параметри;
викликає необхідну бізнес-логіку;
повертає результат.
Зазвичай бізнес-логіку розміщують у сервісах, а контролер лише координує виклик:
import { Controller, Get, Param } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string) {
// У реальному застосунку тут зазвичай викликається UsersService
return {
id,
name: 'Olena',
};
}
}Після завершення контролера результат повертається назад через interceptor до клієнта.
Exception filter обробляє помилки, які не були оброблені раніше.
Filter може:
визначити HTTP-статус;
сформувати єдиний формат помилки;
записати помилку в журнал;
приховати внутрішні деталі помилки від клієнта.
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}Filter не є обов’язковим кроком для успішного запиту. Він спрацьовує лише тоді, коли під час обробки виникає відповідний виняток.
Наприклад, якщо pipe викине BadRequestException, exception filter зможе сформувати відповідь для клієнта.
Нижче наведено мінімальний приклад, у якому один запит проходить через усі основні етапи.
Файл src/main.ts:
import 'reflect-metadata';
import {
ArgumentsHost,
BadRequestException,
CallHandler,
CanActivate,
Controller,
ExceptionFilter,
ExecutionContext,
Get,
Injectable,
MiddlewareConsumer,
Module,
NestInterceptor,
NestMiddleware,
NestModule,
Param,
PipeTransform,
UseFilters,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { NextFunction, Request, Response } from 'express';
import { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators';
// Middleware виконується першим.
@Injectable()
class RequestLogMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[Middleware] ${req.method} ${req.originalUrl}`);
// Передаємо запит далі.
next();
}
}
// Guard перевіряє, чи дозволено доступ до маршруту.
@Injectable()
class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
console.log('[Guard] Перевірка API-ключа');
return request.headers['x-api-key'] === 'secret-key';
}
}
// Interceptor виконує код до та після контролера.
@Injectable()
class LoggingInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const startedAt = Date.now();
console.log('[Interceptor] До контролера');
return next.handle().pipe(
finalize(() => {
const duration = Date.now() - startedAt;
console.log(
`[Interceptor] Після контролера: ${duration} мс`,
);
}),
);
}
}
// Pipe перевіряє та перетворює параметр id.
@Injectable()
class PositiveIntegerPipe
implements PipeTransform<string, number>
{
transform(value: string): number {
console.log('[Pipe] Перевірка параметра id');
const id = Number(value);
if (!Number.isInteger(id) || id <= 0) {
throw new BadRequestException(
'id має бути додатним цілим числом',
);
}
return id;
}
}
// Filter формує відповідь для помилки.
@Catch(BadRequestException)
class BadRequestFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
console.log('[Filter] Обробка помилки');
const response = host.switchToHttp().getResponse<Response>();
response.status(exception.getStatus()).json({
statusCode: exception.getStatus(),
message: exception.message,
});
}
}
@Controller('items')
@UseGuards(ApiKeyGuard)
@UseInterceptors(LoggingInterceptor)
@UseFilters(BadRequestFilter)
class ItemsController {
@Get(':id')
findOne(@Param('id', PositiveIntegerPipe) id: number) {
console.log('[Controller] Обробка маршруту');
return {
id,
name: 'Notebook',
};
}
}
@Module({
controllers: [ItemsController],
})
class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(RequestLogMiddleware)
.forRoutes(ItemsController);
}
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
console.log('Сервер запущено на http://localhost:3000');
}
bootstrap();Для успішного запиту потрібно передати правильний заголовок:
curl -H "x-api-key: secret-key" http://localhost:3000/items/10Очікувана відповідь:
{
"id": 10,
"name": "Notebook"
}Приблизний порядок повідомлень у консолі:
[Middleware] GET /items/10
[Guard] Перевірка API-ключа
[Interceptor] До контролера
[Pipe] Перевірка параметра id
[Controller] Обробка маршруту
[Interceptor] Після контролера: 1 мсЯкщо передати неправильний параметр:
curl -H "x-api-key: secret-key" http://localhost:3000/items/wrongPipe викине помилку, контролер не буде викликано, а filter сформує відповідь:
{
"statusCode": 400,
"message": "id має бути додатним цілим числом"
}Якщо не передати API-ключ або передати неправильний ключ, guard зупинить запит ще до interceptor, pipe та контролера:
curl http://localhost:3000/items/10У цьому випадку клієнт отримає відповідь із забороною доступу.
NestJS дозволяє підключати middleware, guards, interceptors, pipes і filters на різних рівнях:
глобальний — для всього застосунку;
рівень контролера — для всіх маршрутів контролера;
рівень маршруту — лише для одного методу.
Наприклад, декоратор:
@UseGuards(ApiKeyGuard)на класі контролера застосує guard до всіх його маршрутів.
Якщо той самий guard потрібен лише для одного маршруту, декоратор розміщують над методом:
@Get(':id')
@UseGuards(ApiKeyGuard)
findOne(@Param('id') id: string) {
return { id };
}Це дає змогу не застосовувати перевірку там, де вона не потрібна.
next() у middlewareЯкщо middleware не викликає next() і не надсилає власну відповідь, запит зависне.
use(req: Request, res: Response, next: NextFunction) {
console.log('Запит отримано');
// Без next() обробка не продовжиться.
next();
}Middleware може перевіряти заголовки, але для авторизації зазвичай краще використовувати guards. Guard має доступ до контексту виконання NestJS і призначений саме для рішення, чи можна продовжувати обробку маршруту.
Якщо pipe викинув виняток, метод контролера не буде викликано. Некоректні дані потрібно виправляти до входу в контролер або обробляти помилку через filter.
Pipe може одночасно перевірити та перетворити значення. Наприклад, параметр id приходить із URL як рядок, але після pipe контролер отримує число.
Interceptor може виконувати перевірки, але його основне призначення — обгортання виконання, перетворення результату та додаткові дії до або після обробника. Для дозволу або заборони доступу використовуйте guard.
Filter має відповідати типу винятку, для якого він зареєстрований. Наприклад, filter із @Catch(BadRequestException) не призначений для обробки NotFoundException.
Middleware виконується на початку й готує запит до подальшої обробки.
Guards вирішують, чи має запит право продовжувати виконання.
Interceptors виконують код до та після контролера.
Pipes перевіряють і перетворюють параметри, тіло та інші вхідні дані.
Контролер виконує обробник маршруту та повертає результат.
Exception filters перехоплюють необроблені винятки й формують відповіді про помилки.
Якщо guard зупиняє запит або pipe викидає помилку, наступні звичайні етапи для цього запиту не виконуються.