Пошук уроків, статей та іншого контенту
Простежите шлях HTTP-запиту через middleware, guards, interceptors, pipes, контролер, сервіс і фільтри винятків.
Життєвий цикл запиту — це послідовність етапів, через які проходить HTTP-запит від моменту надходження до сервера до формування відповіді.
У типовому випадку NestJS обробляє запит у такому напрямку:
Middleware
Guards
Interceptors до виконання обробника
Pipes
Метод контролера
Сервіс та інші залежності, викликані контролером
Interceptors після виконання обробника
Формування HTTP-відповіді
Exception filters, якщо під час обробки виникла необроблена помилка
Спрощена схема:
HTTP-запит
↓
Middleware
↓
Guards
↓
Interceptors: частина до next.handle()
↓
Pipes
↓
Контролер
↓
Сервіс
↓
Interceptors: частина після next.handle()
↓
HTTP-відповідьЯкщо на будь-якому етапі виникає виняток, звичайний шлях переривається, а NestJS передає помилку до відповідного exception filter.
Middleware — це функція або клас, який виконується до маршрутизації запиту до конкретного методу контролера.
Middleware часто використовують для:
логування запитів;
додавання даних до об’єкта request;
перевірки загальних умов;
роботи з cookie або заголовками;
виконання дій, спільних для кількох маршрутів.
Middleware отримує об’єкти request, response і функцію next. Щоб передати керування наступному етапу, потрібно викликати next().
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
use(request: Request, response: Response, next: NextFunction): void {
console.log(`[middleware] ${request.method} ${request.originalUrl}`);
response.on('finish', () => {
console.log(`[middleware] статус: ${response.statusCode}`);
});
next();
}
}Middleware не знає, який guard або контролер буде виконано далі. Його завдання — підготувати або перевірити запит на ранньому етапі.
Middleware підключають у модулі через метод configure:
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { RequestLoggerMiddleware } from './request-logger.middleware';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(RequestLoggerMiddleware)
.forRoutes('*');
}
}Виклик forRoutes('*') застосовує middleware до всіх маршрутів.
Guard вирішує, чи має запит право продовжити обробку.
Guard виконується після middleware, але до контролера. Якщо guard повертає false або викидає виняток, контролер і сервіс викликані не будуть.
Guards зазвичай використовують для:
автентифікації;
авторизації;
перевірки ролей;
перевірки наявності необхідного заголовка або токена.
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
const apiKey = request.header('x-api-key');
return apiKey === 'development-key';
}
}Якщо guard поверне false, NestJS зазвичай відповість статусом 403 Forbidden.
Guard можна застосувати до конкретного методу:
import { Controller, Get, UseGuards } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get()
@UseGuards(ApiKeyGuard)
findAll() {
return [];
}
}Також guard можна застосувати до всього контролера або зареєструвати глобально.
Interceptor обгортає виконання маршруту. Він може виконати код:
перед викликом контролера;
після успішного виконання контролера;
під час обробки помилки;
перед поверненням відповіді клієнту.
Ключовим є виклик next.handle(). Він передає керування наступному етапу та повертає Observable з результатом обробки запиту.
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() виводиться до контролера;
контролер і сервіс виконуються під час next.handle();
finalize виконується після завершення Observable — як при успіху, так і при помилці.
Interceptors часто використовують для:
вимірювання тривалості запиту;
зміни формату відповіді;
додавання метаданих;
кешування;
централізованого логування.
Pipe обробляє вхідні дані перед передачею їх до методу контролера.
Pipe може:
трансформувати значення;
перевіряти його коректність;
викинути виняток, якщо значення не відповідає очікуваному формату.
Наприклад, pipe може перетворити параметр маршруту з рядка на число:
import {
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class ParseIdPipe implements PipeTransform<string, number> {
transform(value: string): number {
const id = Number(value);
if (!Number.isInteger(id) || id <= 0) {
throw new BadRequestException('Ідентифікатор має бути додатним цілим числом');
}
return id;
}
}Застосування pipe до параметра:
import { Controller, Get, Param } from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id', ParseIdPipe) id: number) {
return { id };
}
}Важливо, що id у URL спочатку є рядком. Pipe перетворює його на число до виклику методу findOne.
NestJS також має вбудовані pipes, наприклад:
ParseIntPipe;
ParseBoolPipe;
ParseFloatPipe;
DefaultValuePipe;
ValidationPipe.
Контролер відповідає за обробку HTTP-маршруту. Він отримує вже підготовлені дані від NestJS і вирішує, яку операцію потрібно виконати.
Контролер не повинен містити складну бізнес-логіку. Зазвичай він:
отримує параметри запиту;
передає їх сервісу;
повертає результат сервісу.
import {
Controller,
Get,
Param,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
@Controller('users')
@UseGuards(ApiKeyGuard)
@UseInterceptors(TimingInterceptor)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
findOne(@Param('id', ParseIdPipe) id: number) {
return this.usersService.findOne(id);
}
}На цьому етапі guards і interceptors контролера вже виконали свою підготовчу частину, а pipe перетворив параметр id.
Сервіс — це звичайний клас із бізнес-логікою. NestJS не має окремого глобального етапу «виконання сервісу».
Сервіс викликається тоді, коли це явно робить контролер або інша залежність. Тому в життєвому циклі запиту сервіс є частиною виконання методу контролера.
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly users = [
{ id: 1, name: 'Олена' },
{ id: 2, name: 'Андрій' },
];
findOne(id: number) {
const user = this.users.find((item) => item.id === id);
if (!user) {
throw new NotFoundException('Користувача не знайдено');
}
return user;
}
}Якщо сервіс викидає виняток, метод контролера не повертає звичайний результат. Помилка передається назад через interceptor-ланцюжок до exception filters.
Exception filter відповідає за обробку винятків і перетворення їх на HTTP-відповідь.
Фільтр може:
визначати статус відповіді;
формувати єдиний формат помилок;
додавати час або ідентифікатор запиту;
логувати винятки.
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost): void {
const context = host.switchToHttp();
const response = context.getResponse();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
const message =
typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as { message?: string | string[] }).message;
response.status(status).json({
statusCode: status,
message,
timestamp: new Date().toISOString(),
});
}
}Фільтр можна застосувати до конкретного маршруту:
import { Get, UseFilters } from '@nestjs/common';
@Get(':id')
@UseFilters(HttpErrorFilter)
findOne(@Param('id', ParseIdPipe) id: number) {
return this.usersService.findOne(id);
}Якщо ParseIdPipe викине BadRequestException або сервіс викине NotFoundException, exception filter сформує відповідь замість звичайного результату.
Нижче наведено мінімальний приклад для NestJS-проєкту. Він демонструє всі основні етапи: middleware, guard, interceptor, pipe, контролер, сервіс і filter.
// src/request-logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
use(request: Request, response: Response, next: NextFunction): void {
console.log(`[middleware] ${request.method} ${request.originalUrl}`);
response.on('finish', () => {
console.log(`[middleware] статус: ${response.statusCode}`);
});
next();
}
}
// src/api-key.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
import { Request } from 'express';
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
return request.header('x-api-key') === 'development-key';
}
}
// src/timing.interceptor.ts
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(() => {
console.log(
`[interceptor] після контролера: ${Date.now() - startedAt} мс`,
);
}),
);
}
}
// src/parse-id.pipe.ts
import {
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class ParseIdPipe implements PipeTransform<string, number> {
transform(value: string): number {
const id = Number(value);
if (!Number.isInteger(id) || id <= 0) {
throw new BadRequestException('id має бути додатним цілим числом');
}
return id;
}
}
// src/http-error.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
} from '@nestjs/common';
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost): void {
const response = host.switchToHttp().getResponse();
const status = exception.getStatus();
const body = exception.getResponse();
response.status(status).json({
statusCode: status,
message: typeof body === 'string' ? body : body['message'],
timestamp: new Date().toISOString(),
});
}
}
// src/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly users = [
{ id: 1, name: 'Олена' },
{ id: 2, name: 'Андрій' },
];
findOne(id: number) {
const user = this.users.find((item) => item.id === id);
if (!user) {
throw new NotFoundException('Користувача не знайдено');
}
return user;
}
}
// src/users.controller.ts
import {
Controller,
Get,
Param,
UseFilters,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { ApiKeyGuard } from './api-key.guard';
import { HttpErrorFilter } from './http-error.filter';
import { ParseIdPipe } from './parse-id.pipe';
import { TimingInterceptor } from './timing.interceptor';
import { UsersService } from './users.service';
@Controller('users')
@UseGuards(ApiKeyGuard)
@UseInterceptors(TimingInterceptor)
@UseFilters(HttpErrorFilter)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
findOne(@Param('id', ParseIdPipe) id: number) {
return this.usersService.findOne(id);
}
}
// src/app.module.ts
import {
MiddlewareConsumer,
Module,
NestModule,
} from '@nestjs/common';
import { RequestLoggerMiddleware } from './request-logger.middleware';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer
.apply(RequestLoggerMiddleware)
.forRoutes(UsersController);
}
}
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Після запуску запит:
curl -H "x-api-key: development-key" http://localhost:3000/users/1пройде приблизно так:
Middleware виведе метод і URL.
ApiKeyGuard перевірить заголовок.
TimingInterceptor виконає код до next.handle().
ParseIdPipe перетворить "1" на 1.
Метод контролера викличе UsersService.
Сервіс поверне користувача.
Interceptor виконає код після завершення обробки.
Middleware виведе фінальний статус відповіді.
Якщо виконати запит до неіснуючого користувача:
curl -H "x-api-key: development-key" http://localhost:3000/users/99сервіс викине NotFoundException, а HttpErrorFilter сформує відповідь зі статусом 404.
Якщо не передати API-ключ:
curl http://localhost:3000/users/1guard зупинить обробку. Pipe, контролер і сервіс викликані не будуть.
Один і той самий компонент можна підключити на різних рівнях:
глобальний рівень — для всього застосунку;
рівень контролера — для всіх методів конкретного контролера;
рівень маршруту — для одного методу або параметра.
Наприклад:
@UseInterceptors(TimingInterceptor)
@Controller('users')
export class UsersController {
// Interceptor застосовується до всіх маршрутів контролера
}Або:
@Get(':id')
@UseGuards(ApiKeyGuard)
findOne() {
// Guard застосовується лише до цього маршруту
}Чим ближче компонент підключений до конкретного маршруту, тим вужча його область дії.
Помилка може виникнути на різних етапах:
middleware може самостійно викинути виняток;
guard може заборонити доступ;
pipe може відхилити некоректні дані;
контролер або сервіс можуть викинути HttpException;
interceptor може перехопити помилку під час роботи з Observable.
Якщо помилка не оброблена раніше, NestJS передає її exception filters. Якщо власного фільтра немає, NestJS використає стандартний фільтр винятків.
Важливо розрізняти:
return false у guard — запит зазвичай завершується відповіддю 403;
throw new UnauthorizedException() — можна явно повернути 401;
throw new BadRequestException() у pipe — дані запиту вважаються некоректними;
throw new NotFoundException() у сервісі — ресурс не знайдено.
next() двічіMiddleware має передати керування далі лише один раз.
use(request: Request, response: Response, next: NextFunction): void {
next();
// Повторний виклик next() тут є помилкою
}Guard повинен вирішувати, чи дозволено виконання запиту. Складні операції з даними краще передавати сервісу.
Pipe обробляє лише ті значення, до яких його застосовано. Наприклад, pipe для @Param('id') не перевіряє тіло запиту.
next.handle() в interceptorЯкщо interceptor не викличе next.handle(), наступні етапи ланцюжка не виконаються, і запит може залишитися без відповіді.
NestJS не викликає всі сервіси автоматично. Сервіс виконується лише тоді, коли його викликає контролер або інша залежність.
Exception filter має формувати безпечну відповідь. Стек викликів, SQL-запити та внутрішні налаштування не повинні потрапляти до клієнта у production.
Middleware виконується найраніше та працює із загальним HTTP-запитом.
Guards вирішують, чи дозволено продовжувати обробку.
Interceptors обгортають виконання маршруту та можуть працювати до і після контролера.
Pipes трансформують і перевіряють вхідні значення.
Контролер координує обробку маршруту.
Сервіс виконує бізнес-логіку, викликану контролером.
Exception filters перетворюють необроблені винятки на HTTP-відповіді.
Якщо guard або pipe завершує обробку запиту помилкою, контролер і сервіс не виконуються.
Розуміння порядку виконання допомагає правильно розподіляти відповідальність між компонентами NestJS.