Пошук уроків, статей та іншого контенту
Реалізуєте interceptor для журналювання методу, маршруту, тривалості та результату HTTP-запиту.
Interceptor у NestJS може виконувати код:
до виклику методу контролера;
після успішного завершення методу;
під час обробки помилки.
Для журналювання HTTP-запиту interceptor зручно використовувати, щоб централізовано отримувати:
HTTP-метод;
маршрут або URL;
тривалість виконання;
статус відповіді;
результат: успішне завершення або помилка.
На відміну від додавання Logger.log() у кожен метод контролера, interceptor працює для багатьох маршрутів одразу.
Interceptor реалізує інтерфейс NestInterceptor. Його основний метод — intercept():
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
return next.handle();
}ExecutionContext містить інформацію про поточний контекст виконання.
CallHandler дає змогу передати виконання далі за допомогою next.handle().
next.handle() повертає Observable, до якого можна під’єднати оператори RxJS.
Для виконання коду після завершення запиту використаємо оператор tap().
Щоб отримати об’єкти запиту та відповіді, потрібно переключити контекст на HTTP:
const http = context.switchToHttp();
const request = http.getRequest();
const response = http.getResponse();З об’єкта запиту можна отримати:
request.method — HTTP-метод;
request.originalUrl — повний URL у Express;
request.url — URL запиту.
З об’єкта відповіді можна отримати:
response.statusCode — HTTP-статус відповіді.
Для маршруту краще спочатку використати request.route?.path. Він містить шаблон маршруту, наприклад /users/:id. Якщо він недоступний, можна використати фактичний URL.
Створимо файл logging.interceptor.ts:
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
type HttpRequest = {
method: string;
originalUrl?: string;
url?: string;
route?: {
path?: string;
};
};
type HttpResponse = {
statusCode: number;
};
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const httpContext = context.switchToHttp();
const request = httpContext.getRequest<HttpRequest>();
const response = httpContext.getResponse<HttpResponse>();
const method = request.method;
const route = request.route?.path ?? request.originalUrl ?? request.url ?? '';
const startedAt = Date.now();
return next.handle().pipe(
tap({
next: () => {
const duration = Date.now() - startedAt;
this.logger.log(
`${method} ${route} ${response.statusCode} - успішно - ${duration} мс`,
);
},
error: (error: unknown) => {
const duration = Date.now() - startedAt;
const errorMessage =
error instanceof Error ? error.message : String(error);
this.logger.error(
`${method} ${route} ${response.statusCode} - помилка - ${duration} мс - ${errorMessage}`,
);
},
}),
);
}
}Перед викликом контролера interceptor:
отримує HTTP-запит і відповідь;
зберігає метод і маршрут;
фіксує час початку виконання.
Після виконання обробника:
next виконується, якщо запит завершився успішно;
error виконується, якщо обробник викинув помилку;
тривалість обчислюється як різниця між поточним часом і часом початку.
Приклад повідомлення в журналі:
[Nest] 12345 - 06/15/2026, 10:30:00 AM LOG [LoggingInterceptor] GET /users/:id 200 - успішно - 12 мсДля помилки:
[Nest] 12345 - 06/15/2026, 10:30:01 AM ERROR [LoggingInterceptor] GET /users/:id 500 - помилка - 8 мс - User not foundЯкщо журналювання потрібне для всіх HTTP-маршрутів застосунку, підключіть interceptor у main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);
// Підключаємо журналювання для всіх HTTP-запитів
app.useGlobalInterceptors(new LoggingInterceptor());
await app.listen(3000);
}
void bootstrap();Після цього interceptor буде викликатися для кожного маршруту, зареєстрованого в застосунку.
Наприклад, для контролера:
import { Controller, Get } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
getHealth(): { status: string } {
return { status: 'ok' };
}
}Запит:
GET http://localhost:3000/healthдасть журнал приблизно такого вигляду:
GET /health 200 - успішно - 3 мсІноді журналювання для всього застосунку не потрібне. Interceptor можна застосувати лише до одного контролера за допомогою @UseInterceptors():
import {
Controller,
Get,
UseInterceptors,
} from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
@Controller('orders')
@UseInterceptors(LoggingInterceptor)
export class OrdersController {
@Get()
findAll(): { items: string[] } {
return {
items: ['order-1', 'order-2'],
};
}
}У цьому випадку interceptor працюватиме для всіх методів OrdersController, але не для інших контролерів.
Його також можна застосувати лише до одного методу:
import {
Controller,
Get,
UseInterceptors,
} from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
@Controller('reports')
export class ReportsController {
@Get()
@UseInterceptors(LoggingInterceptor)
getReport(): { generated: boolean } {
return {
generated: true,
};
}
}APP_INTERCEPTORГлобальний interceptor також можна зареєструвати як провайдер у модулі:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './logging.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}Такий спосіб корисний, коли interceptor має залежності з інших модулів і повинен отримувати їх через dependency injection.
Якщо interceptor не має залежностей, app.useGlobalInterceptors(new LoggingInterceptor()) у main.ts є простішим варіантом.
У прикладі next лише фіксує факт успішного виконання. Це навмисно: записувати в журнал усе тіло відповіді не завжди безпечно.
Відповідь може містити:
персональні дані;
токени;
паролі або секрети;
великі масиви даних.
Для HTTP-журналу зазвичай достатньо записувати:
метод;
маршрут;
статус;
тривалість;
результат виконання.
Якщо потрібно перевірити саме значення, яке повернув обробник, tap може прийняти його аргумент:
return next.handle().pipe(
tap((result: unknown) => {
this.logger.log(
`${request.method} ${route} завершено зі значенням типу ${typeof result}`,
);
}),
);Однак повне значення result не варто автоматично записувати в журнал без перевірки його вмісту.
Interceptor не повинен поглинати помилку. У callback error ми лише записуємо інформацію, але не повертаємо інше значення і не замінюємо помилку.
Завдяки цьому стандартний механізм NestJS продовжує обробляти виняток і формує HTTP-відповідь для клієнта.
Важливо розрізняти:
next — Observable успішно повернув результат;
error — під час виконання виникла помилка;
response.statusCode — статус відповіді, доступний на момент журналювання.
У деяких випадках під час error статус відповіді ще може мати початкове значення, оскільки exception filter обробить помилку пізніше. Якщо потрібен гарантовано точний статус винятку, його можна отримати з самого винятку, коли це передбачено типом помилки.
next.handle()Якщо interceptor не повертає next.handle(), метод контролера не буде виконаний:
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
// Помилка: виконання запиту не передається далі
return new Observable();
}У звичайному interceptor потрібно повертати Observable, отриманий від next.handle().
subscribe() всередині interceptorНе потрібно вручну підписуватися на Observable:
next.handle().subscribe();Це може порушити стандартний життєвий цикл обробки запиту. Для побічних дій, зокрема журналювання, використовуйте оператори RxJS, наприклад tap().
Не додавайте до журналу заголовок Authorization, паролі, токени або повне тіло відповіді без чіткої потреби.
Журнал має допомагати діагностувати роботу системи, а не створювати додаткове джерело витоку даних.
request.originalUrl може містити конкретне значення:
/users/42А request.route?.path може містити шаблон:
/users/:idДля агрегування статистики за маршрутами зручніший шаблон. Для аналізу конкретного запиту — фактичний URL.
HTTP-обробник зазвичай повертає одне значення, але загалом Observable може видавати кілька значень. Якщо журналювання має відбуватися один раз на HTTP-запит, переконайтеся, що ви використовуєте відповідну логіку завершення Observable.
LoggingInterceptor централізує журналювання HTTP-запитів.
ExecutionContext дає доступ до запиту та відповіді через switchToHttp().
next.handle() запускає подальше виконання маршруту.
tap() дає змогу записати успішний результат або помилку.
Тривалість запиту можна визначити за часом до і після next.handle().
Для всіх маршрутів interceptor можна підключити глобально.
Не слід записувати в журнал конфіденційні дані або повне тіло відповіді без потреби.