Пошук уроків, статей та іншого контенту
Порівняєте призначення та порядок виконання middleware, guards, pipes, interceptors і filters.
У NestJS обробка HTTP-запиту може проходити через кілька рівнів:
Middleware — працює із запитом до вибору маршруту.
Guards — вирішує, чи дозволено виконувати обробник.
Interceptors — обгортає виконання обробника та може обробити результат.
Pipes — перетворює і перевіряє вхідні дані.
Controller — виконує основну логіку маршруту.
Exception filters — формує відповідь, якщо виникла помилка.
Спрощена схема:
HTTP-запит
↓
Middleware
↓
Guards
↓
Interceptors: до виконання
↓
Pipes
↓
Controller
↓
Interceptors: після виконання
↓
HTTP-відповідь
Якщо виникла помилка:
Exception filterFilters не запускаються для успішних запитів. Вони потрібні для обробки винятків.
Middleware — це функція або клас, який отримує HTTP-запит, HTTP-відповідь і функцію next.
Middleware може:
додати дані до об’єкта запиту;
записати лог;
перевірити або змінити заголовки;
виконати загальну підготовку запиту;
передати керування далі через next().
Middleware не знає про конкретний метод контролера так само добре, як guards, pipes або interceptors. Його основна область відповідальності — загальна обробка HTTP-запитів.
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class RequestLogMiddleware implements NestMiddleware {
use(request: any, response: any, next: () => void) {
console.log(`${request.method} ${request.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 добре підходить для:
журналювання всіх запитів;
додавання ідентифікатора запиту;
загальної роботи із заголовками;
простих HTTP-перевірок, які не залежать від метаданих маршруту.
Для перевірки прав доступу зазвичай краще використовувати guard.
Guard відповідає на запитання: чи можна виконувати цей маршрут?
Guard реалізує інтерфейс CanActivate і повертає:
true, якщо доступ дозволено;
false, якщо доступ заборонено;
або кидає виняток.
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 виконується після middleware, але до interceptor і pipe. Якщо guard повертає false, контролер не буде викликаний.
Guard може отримати інформацію про:
HTTP-запит;
поточний контролер;
поточний метод;
метадані, додані декораторами.
Це робить guards зручними для:
автентифікації;
авторизації;
перевірки ролей;
перевірки доступу до конкретного ресурсу.
Guard можна застосувати до одного маршруту:
@UseGuards(ApiKeyGuard)
@Get(':id')
findOne(@Param('id') id: number) {
return { id };
}Якщо для всіх маршрутів модуля потрібна однакова перевірка, guard можна застосувати на рівні контролера або глобально.
Pipe працює з вхідними даними методу контролера. Він може:
трансформувати значення;
перевіряти значення;
кинути виняток, якщо дані неправильні.
Pipe реалізує інтерфейс PipeTransform.
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class PositiveIntPipe implements PipeTransform {
transform(value: string, metadata: ArgumentMetadata): number {
const numberValue = Number(value);
if (!Number.isInteger(numberValue) || numberValue <= 0) {
throw new BadRequestException(
'Параметр id має бути додатним цілим числом',
);
}
return numberValue;
}
}Pipe можна застосувати до параметра:
@Get(':id')
findOne(@Param('id', PositiveIntPipe) id: number) {
return { id };
}У цьому прикладі значення з URL спочатку є рядком. PositiveIntPipe перевіряє його і передає в контролер уже як число.
Pipes часто застосовують до:
параметрів маршруту;
query-параметрів;
тіла запиту;
перевірки DTO.
Вбудовані pipes NestJS, наприклад ParseIntPipe, можуть виконувати типові перетворення та перевірки.
Interceptor обгортає виконання маршруту. Він може виконати код:
до виклику контролера;
після отримання результату;
під час обробки помилки.
Interceptor реалізує інтерфейс NestInterceptor і працює з CallHandler.
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
console.log('До виконання контролера');
return next.handle().pipe(
map((data) => {
console.log('Після виконання контролера');
return {
data,
};
}),
);
}
}next.handle() запускає подальше виконання маршруту. Його результатом є Observable, тому результат можна змінювати операторами RxJS.
Interceptors підходять для:
уніфікації формату успішних відповідей;
вимірювання часу виконання;
кешування;
додавання логування навколо обробника;
перетворення результату контролера.
Interceptor відрізняється від middleware тим, що він безпосередньо обгортає виконання маршруту та має доступ до його результату.
Exception filter обробляє винятки, які виникли під час обробки запиту, і перетворює їх на HTTP-відповідь.
Filter реалізує ExceptionFilter. Декоратор @Catch() визначає, які винятки він обробляє.
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 request = host.switchToHttp().getRequest();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
path: request.url,
message: exception.message,
});
}
}Filter не відповідає за дозвіл доступу, перетворення параметрів або успішні результати. Його відповідальність — помилки.
Filters корисні, коли потрібно:
мати єдиний формат помилок;
приховати внутрішні деталі винятку;
додати URL або ідентифікатор запиту до помилки;
централізовано обробляти конкретний тип винятків.
Нижче наведено мінімальний приклад, у якому всі механізми працюють разом.
import {
ArgumentMetadata,
BadRequestException,
CallHandler,
CanActivate,
Catch,
ExecutionContext,
ExceptionFilter,
Injectable,
MiddlewareConsumer,
Module,
NestFactory,
NestInterceptor,
NestMiddleware,
NestModule,
PipeTransform,
UseGuards,
UseInterceptors,
Controller,
Get,
Param,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
@Injectable()
export class RequestLogMiddleware implements NestMiddleware {
use(request: any, response: any, next: () => void) {
console.log('1. Middleware: початок');
next();
console.log('6. Middleware: завершення');
}
}
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
console.log('2. Guard');
const request = context.switchToHttp().getRequest();
return request.headers['x-api-key'] === 'secret-key';
}
}
@Injectable()
export class PositiveIntPipe implements PipeTransform {
transform(value: string, metadata: ArgumentMetadata): number {
console.log('4. Pipe');
const numberValue = Number(value);
if (!Number.isInteger(numberValue) || numberValue <= 0) {
throw new BadRequestException('id має бути додатним цілим числом');
}
return numberValue;
}
}
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
console.log('3. Interceptor: до контролера');
return next.handle().pipe(
map((data) => {
console.log('5. Interceptor: після контролера');
return {
data,
};
}),
);
}
}
@Controller('users')
@UseGuards(ApiKeyGuard)
@UseInterceptors(ResponseInterceptor)
export class UsersController {
@Get(':id')
findOne(@Param('id', PositiveIntPipe) id: number) {
console.log('Контролер');
return {
id,
name: 'Olena',
};
}
}
@Module({
controllers: [UsersController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(RequestLogMiddleware)
.forRoutes(UsersController);
}
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Для запиту з правильним заголовком:
GET /users/42
x-api-key: secret-keyпослідовність буде приблизно такою:
1. Middleware: початок
2. Guard
3. Interceptor: до контролера
4. Pipe
Контролер
5. Interceptor: після контролера
6. Middleware: завершенняЯкщо ApiKeyGuard заборонить доступ, pipe, контролер і частина interceptor після next.handle() не виконаються.
Якщо pipe викине BadRequestException, контролер також не буде викликаний. Для єдиного формату такої помилки можна додати exception filter:
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 request = host.switchToHttp().getRequest();
response.status(exception.getStatus()).json({
statusCode: exception.getStatus(),
message: exception.message,
path: request.url,
});
}
}Глобальна реєстрація filter у bootstrap:
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpErrorFilter());
await app.listen(3000);Запитання:
Що потрібно зробити для HTTP-запиту до передачі керування маршруту?
Приклад: записати лог усіх запитів.
Запитання:
Чи має цей запит право виконувати маршрут?
Приклад: перевірити API-ключ або роль користувача.
Запитання:
Чи правильні вхідні дані та в якому вигляді їх передати контролеру?
Приклад: перетворити id із рядка на число.
Запитання:
Що потрібно виконати навколо контролера або його результату?
Приклад: загорнути успішну відповідь у поле data.
Запитання:
Як перетворити виняток на HTTP-відповідь?
Приклад: повернути всі помилки в єдиному форматі.
Middleware може перевіряти загальні умови, але для авторизації зазвичай потрібен guard. Guard має доступ до контексту виконання NestJS і краще відповідає моделі «дозволити або заборонити маршрут».
Guard не повинен перевіряти коректність body, params або query. Його завдання — прийняти рішення щодо доступу. Перевірку та перетворення вхідних даних слід виконувати в pipe.
Interceptor може працювати з результатом і помилками всередині потоку виконання, але filter призначений саме для централізованого формування відповідей на винятки. Це різні рівні відповідальності.
next() у middlewareЯкщо middleware не викликає next() і не завершує відповідь, наступні етапи не будуть виконані.
Якщо pipe має трансформувати значення, результат transform() потрібно повернути. Інакше контролер отримає початкове значення.
transform(value: string): number {
return Number(value);
}Exception filter не запускається для звичайної успішної відповіді. Він активується лише тоді, коли виник виняток, який потрібно обробити.
Middleware обробляє HTTP-запит перед іншими механізмами.
Guard вирішує, чи дозволено виконання маршруту.
Interceptor виконує код до і після обробника та може змінювати результат.
Pipe перевіряє або трансформує вхідні дані.
Exception filter перетворює винятки на HTTP-відповіді.
Типовий порядок виконання: middleware → guard → interceptor → pipe → controller → interceptor після виконання.
Filters застосовуються для обробки помилок, а не для успішних запитів.
Вибирайте механізм за його відповідальністю, а не лише за місцем, де зручно розмістити код.