Пошук уроків, статей та іншого контенту
Побудуєте повний конвеєр запиту, поєднавши автентифікацію, логування, трансформацію відповіді та обробку помилок.
У NestJS кожен компонент життєвого циклу відповідає за окрему частину обробки HTTP-запиту. У реальному застосунку ці компоненти зазвичай працюють разом:
middleware виконує технічну підготовку запиту та базове логування;
guard перевіряє автентифікацію;
interceptor додає логування та уніфікує успішну відповідь;
контролер виконує бізнес-логіку;
exception filter перетворює помилки на єдиний формат HTTP-відповіді.
У цьому уроці ми побудуємо повний конвеєр для HTTP API.
Для звичайного HTTP-запиту спрощений порядок має такий вигляд:
Middleware.
Guards.
Interceptors до виклику обробника.
Pipes.
Метод контролера.
Interceptors після виклику обробника.
Exception filters, якщо виникла необроблена помилка.
Важливо розрізняти відповідальність компонентів:
middleware не має повної інформації про маршрут NestJS;
guard вирішує, чи дозволено виконувати запит;
interceptor
exception filter формує остаточну відповідь для необробленої помилки.
Це означає, що автентифікацію не варто реалізовувати в middleware, а форматування помилок — в interceptor.
У прикладі будуть такі компоненти:
requestLogger — middleware для логування початку та завершення запиту;
AuthGuard — guard для перевірки Bearer-токена;
ResponseInterceptor — interceptor для логування та обгортання успішної відповіді;
ApiExceptionFilter — глобальний filter для єдиного формату помилок;
ReportsController — контролер із захищеними маршрутами.
Для простоти токен буде перевірятися безпосередньо в guard. У production-застосунку перевірка зазвичай делегується окремому сервісу автентифікації.
Middleware виконується до guard і може вимірювати повну тривалість запиту — від моменту його отримання до відправлення відповіді.
// src/common/middleware/request-logger.middleware.ts
import { NextFunction, Request, Response } from 'express';
export function requestLogger(
req: Request,
res: Response,
next: NextFunction,
): void {
const startedAt = Date.now();
console.log(`[request] ${req.method} ${req.originalUrl}`);
res.on('finish', () => {
const duration = Date.now() - startedAt;
console.log(
`[response] ${req.method} ${req.originalUrl} ` +
`${res.statusCode} ${duration}ms`,
);
});
next();
}Middleware не зупиняє запит і не перевіряє права доступу. Його завдання — передати керування далі за допомогою next().
Подію finish генерує об’єкт відповіді Express після завершення її відправлення. Тому middleware побачить і успішну відповідь, і відповідь, сформовану exception filter.
Guard отримує ExecutionContext, з якого можна дістати HTTP-запит. Якщо токен відсутній або некоректний, guard викидає UnauthorizedException.
// src/common/guards/auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
export interface AuthenticatedRequest extends Request {
user?: {
id: number;
email: string;
};
}
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
const authorization = request.headers.authorization;
if (!authorization?.startsWith('Bearer ')) {
throw new UnauthorizedException('Bearer-токен є обов’язковим');
}
const token = authorization.slice('Bearer '.length);
if (token !== 'demo-token') {
throw new UnauthorizedException('Некоректний токен');
}
// Додаємо автентифікованого користувача до запиту
request.user = {
id: 1,
email: 'user@example.com',
};
return true;
}
}Guard повертає true, якщо виконання дозволено. Якщо повернути false, NestJS зазвичай сформує відповідь зі статусом 403 Forbidden. Для відсутнього або некоректного токена доречніше явно викинути UnauthorizedException, щоб отримати статус 401 Unauthorized.
Guard повинен відповідати лише за доступ до маршруту. Він не має:
формувати фінальний JSON-вивід;
виконувати логіку контролера;
перехоплювати всі можливі помилки застосунку;
займатися трансформацією успішної відповіді.
Interceptor отримує керування до виклику контролера, а після next.handle() може обробити результат контролера.
У прикладі interceptor:
запам’ятовує час початку;
викликає контролер;
обгортає успішний результат у поле data;
записує тривалість виконання;
передає помилку далі, щоб її обробив exception filter.
// src/common/interceptors/response.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { Observable, catchError, map, throwError } from 'rxjs';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const request = context.switchToHttp().getRequest<Request>();
const response = context.switchToHttp().getResponse<Response>();
const startedAt = Date.now();
return next.handle().pipe(
map((data: unknown) => {
const duration = Date.now() - startedAt;
console.log(
`[handler] ${request.method} ${request.originalUrl} ` +
`${response.statusCode} ${duration}ms`,
);
return {
success: true,
data,
};
}),
catchError((error: unknown) => {
const duration = Date.now() - startedAt;
console.error(
`[handler-error] ${request.method} ${request.originalUrl} ` +
`${duration}ms`,
);
// Помилка переходить до exception filter
return throwError(() => error);
}),
);
}
}next.handle() повертає RxJS Observable. Оператор map обробляє успішний результат, а catchError — помилку.
Interceptor не повинен перетворювати помилку на успішну відповідь. Якщо в catchError повернути звичайне значення, помилка буде прихована від exception filter.
Exception filter перехоплює необроблені винятки та формує HTTP-відповідь. Декоратор @Catch() без аргументів означає, що filter може обробляти будь-які винятки.
// src/common/filters/api-exception.filter.ts
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class ApiExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void {
const http = host.switchToHttp();
const request = http.getRequest<Request>();
const response = http.getResponse<Response>();
const isHttpException = exception instanceof HttpException;
const status = isHttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const exceptionResponse = isHttpException
? exception.getResponse()
: null;
const message =
typeof exceptionResponse === 'string'
? exceptionResponse
: typeof exceptionResponse === 'object' &&
exceptionResponse !== null &&
'message' in exceptionResponse
? exceptionResponse.message
: 'Внутрішня помилка сервера';
response.status(status).json({
success: false,
error: {
statusCode: status,
message,
path: request.originalUrl,
timestamp: new Date().toISOString(),
},
});
}
}Filter розрізняє два типи помилок:
HttpException — очікувана помилка NestJS із конкретним HTTP-статусом;
будь-яка інша помилка — внутрішня помилка сервера зі статусом 500.
Не варто повертати клієнту exception.stack або повний текст невідомої помилки. Stack trace може містити внутрішню структуру застосунку та конфіденційні дані.
Контролер використовує дані, які guard додав до запиту. Маршрут me повертає поточного користувача, а маршрут reports/:id демонструє обробку помилки NotFoundException.
// src/reports/reports.controller.ts
import {
Controller,
Get,
NotFoundException,
Param,
Req,
} from '@nestjs/common';
import { AuthenticatedRequest } from '../common/guards/auth.guard';
@Controller('reports')
export class ReportsController {
@Get('me')
getCurrentUser(@Req() request: AuthenticatedRequest) {
return {
user: request.user,
};
}
@Get(':id')
getReport(@Param('id') id: string) {
if (id !== '1') {
throw new NotFoundException(`Звіт ${id} не знайдено`);
}
return {
id: 1,
title: 'Місячний звіт',
status: 'ready',
};
}
}Метод контролера не займається обгортанням відповіді у формат { success, data }. Це централізована відповідальність interceptor.
Так само контролер не формує JSON для помилок. Він лише викидає відповідний виняток, а єдиний filter визначає структуру фінальної відповіді.
Нижче наведено повний мінімальний приклад застосунку.
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ReportsController } from './reports/reports.controller';
@Module({
controllers: [ReportsController],
})
export class AppModule {}// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AuthGuard } from './common/guards/auth.guard';
import { ApiExceptionFilter } from './common/filters/api-exception.filter';
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
import { requestLogger } from './common/middleware/request-logger.middleware';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
// Middleware застосовується до всіх HTTP-запитів
app.use(requestLogger);
// Guard, interceptor і filter застосовуються глобально
app.useGlobalGuards(new AuthGuard());
app.useGlobalInterceptors(new ResponseInterceptor());
app.useGlobalFilters(new ApiExceptionFilter());
await app.listen(3000);
}
void bootstrap();У цьому прикладі глобальні компоненти створюються через new. Це підходить для компонентів без залежностей у конструкторі.
Якщо guard, interceptor або filter потребує сервісів NestJS, краще зареєструвати його через APP_GUARD, APP_INTERCEPTOR або APP_FILTER у модулі. Тоді залежності будуть створені контейнером NestJS.
Після запуску застосунку можна виконати запити.
curl -i \
-H "Authorization: Bearer demo-token" \
http://localhost:3000/reports/meОчікувана відповідь:
{
"success": true,
"data": {
"user": {
"id": 1,
"email": "user@example.com"
}
}
}У консолі застосунку з’являться записи від middleware та interceptor:
[request] GET /reports/me
[handler] GET /reports/me 200 1ms
[response] GET /reports/me 200 3mscurl -i http://localhost:3000/reports/meОчікувана відповідь:
{
"success": false,
"error": {
"statusCode": 401,
"message": "Bearer-токен є обов’язковим",
"path": "/reports/me",
"timestamp": "2026-09-01T12:00:00.000Z"
}
}У цьому випадку:
middleware записує початок запиту;
guard зупиняє виконання;
контролер не викликається;
exception filter формує відповідь;
middleware записує завершення запиту.
Interceptor, який обгортає результат контролера, не отримує успішного результату, оскільки guard виконується раніше.
curl -i \
-H "Authorization: Bearer demo-token" \
http://localhost:3000/reports/999Очікувана відповідь:
{
"success": false,
"error": {
"statusCode": 404,
"message": "Звіт 999 не знайдено",
"path": "/reports/999",
"timestamp": "2026-09-01T12:00:00.000Z"
}
}У цьому випадку guard пропускає запит, контролер викидає NotFoundException, interceptor передає помилку далі, а filter формує відповідь зі статусом 404.
Для запиту GET /reports/1 із правильним токеном послідовність виглядає так:
requestLogger записує метод і URL.
AuthGuard читає заголовок Authorization.
AuthGuard додає user до об’єкта запиту.
ResponseInterceptor починає вимірювання часу.
ReportsController.getReport() повертає звіт.
ResponseInterceptor обгортає звіт у { success: true, data }.
NestJS надсилає відповідь клієнту.
requestLogger записує HTTP-статус і повну тривалість запиту.
Для запиту без токена кроки 4–6 не виконуються, оскільки guard завершує конвеєр винятком.
Коли компонент має залежності, його можна зареєструвати як глобальний provider.
Наприклад, interceptor можна підключити через APP_INTERCEPTOR:
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ReportsController } from './reports/reports.controller';
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
@Module({
controllers: [ReportsController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ResponseInterceptor,
},
],
})
export class AppModule {}У такому разі з main.ts потрібно прибрати:
app.useGlobalInterceptors(new ResponseInterceptor());Аналогічний підхід застосовується для:
APP_GUARD;
APP_FILTER;
інших provider-компонентів NestJS.
Перевага модульної реєстрації — можливість інжектити сервіси через конструктор:
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly authService: AuthService) {}
canActivate(context: ExecutionContext): boolean {
// Використання authService для перевірки токена
return true;
}
}Централізований filter дозволяє контролерам залишатися простими:
@Get(':id')
getReport(@Param('id') id: string) {
if (id !== '1') {
throw new NotFoundException('Звіт не знайдено');
}
return {
id: 1,
title: 'Місячний звіт',
};
}Не потрібно в кожному методі:
викликати response.status(...);
створювати однакові поля timestamp і path;
перевіряти тип помилки;
формувати різні варіанти JSON для клієнта.
Єдиний filter забезпечує однаковий контракт API для всіх маршрутів.
Порядок глобальної реєстрації в main.ts не змінює базовий життєвий цикл NestJS. Guard усе одно виконується до interceptor, тому interceptor не може замінити перевірку доступу.
catchErrorНеправильний варіант:
catchError(() => {
return of({
success: false,
data: null,
});
});Такий код перетворює помилку на успішний результат. Клієнт може отримати HTTP 200, хоча операція завершилася невдало.
Якщо interceptor лише логуватиме помилку, потрібно передати її далі:
catchError((error: unknown) => {
console.error(error);
return throwError(() => error);
});Якщо глобальний interceptor вже повертає:
{
"success": true,
"data": {}
}не потрібно вручну створювати такий самий формат у кожному контролері. Інакше можна отримати вкладену структуру:
{
"success": true,
"data": {
"success": true,
"data": {}
}
}Не слід записувати в лог повний заголовок Authorization. Логи часто доступні ширшому колу систем і працівників, ніж самі секрети.
Безпечніше логувати лише:
метод;
URL;
статус;
тривалість;
ідентифікатор користувача, якщо це дозволено політикою безпеки.
Exception filter не повинен містити бізнес-логіку. Його завдання — визначити HTTP-статус і формат відповіді. Перевірка прав, пошук сутностей та виконання операцій мають залишатися в guard, сервісах і контролерах.
Для невідомої помилки клієнту потрібно повертати загальне повідомлення на кшталт Внутрішня помилка сервера, а деталі записувати у внутрішній лог. Передавання stack trace клієнту створює ризик розкриття внутрішньої інформації.
Під час комбінування компонентів дотримуйтеся простого поділу:
Middleware — технічні дії до обробки маршруту.
Guard — автентифікація та авторизація.
Interceptor — спільна поведінка до або після обробника.
Controller — прийняття параметрів і виклик бізнес-логіки.
Exception filter — фінальний формат необроблених помилок.
Якщо один компонент починає виконувати обов’язки іншого, конвеєр стає складним для тестування та підтримки.
У повному конвеєрі NestJS компоненти доповнюють один одного:
middleware фіксує весь HTTP-запит;
guard зупиняє неавторизований доступ;
interceptor обробляє успішні результати та логуватиме помилки;
exception filter формує єдиний формат помилок;
контролер залишається зосередженим на обробці маршруту.
Ключова ідея — не виконувати всі завдання в одному компоненті. Чіткий розподіл відповідальності робить поведінку API передбачуваною, а глобальна реєстрація забезпечує однаковий конвеєр для всіх маршрутів.