Пошук уроків, статей та іншого контенту
Дізнаєтеся, як interceptors виконують код до й після обробника та змінюють процес обробки запиту.
Interceptor — це клас, який може виконати код до виклику обробника маршруту та після нього.
Interceptor працює між отриманням HTTP-запиту та виконанням методу контролера. Він отримує:
ExecutionContext — інформацію про поточний запит і контекст виконання;
CallHandler — об’єкт, через який запускається наступний етап обробки, зазвичай метод контролера.
Базова структура interceptor має такий вигляд:
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class ExampleInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
// Код до виконання обробника
const result$ = next.handle();
// result$ містить результат виконання обробника
return result$;
}
}Виклик next.handle() запускає наступний етап обробки, зокрема метод контролера. Оскільки результатом є Observable, після нього можна додати операції RxJS для обробки результату.
Для запиту до маршруту interceptor виконується приблизно так:
Interceptor отримує запит.
Виконується код до next.handle().
Викликається метод контролера.
Результат методу проходить через оператори після next.handle().
Клієнт отримує відповідь.
Наприклад:
intercept(context: ExecutionContext, next: CallHandler) {
console.log('До обробника');
return next.handle().pipe(
tap(() => {
console.log('Після обробника');
}),
);
}Порядок повідомлень буде таким:
До обробника
Метод контролера
Після обробникаОдна з поширених задач interceptor — вимірювання тривалості обробки запиту.
Для цього можна зберегти час перед викликом обробника, а після його завершення обчислити різницю за допомогою оператора tap.
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const startedAt = Date.now();
this.logger.log(`Початок запиту: ${method} ${url}`);
return next.handle().pipe(
tap(() => {
const duration = Date.now() - startedAt;
this.logger.log(
`Завершення запиту: ${method} ${url} (${duration} мс)`,
);
}),
);
}
}tap не змінює значення, яке проходить через Observable. Він призначений для побічних дій: логування, вимірювання часу або збирання метрик.
За допомогою оператора map interceptor може змінити значення, яке повертає контролер.
Наприклад, можна загорнути кожну успішну відповідь у властивість data:
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable } from 'rxjs';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
return next.handle().pipe(
map((data) => ({
data,
})),
);
}
}Якщо метод контролера повертає:
{ id: 1, name: 'Anna' }то після interceptor відповідь матиме вигляд:
{
"data": {
"id": 1,
"name": "Anna"
}
}На відміну від tap, оператор map змінює значення, яке повертається клієнту.
Interceptor можна застосувати до окремого методу, усього контролера або всього застосунку.
Використовуйте декоратор @UseInterceptors() над методом контролера:
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
@Controller('users')
export class UsersController {
@Get()
@UseInterceptors(LoggingInterceptor)
findAll() {
return [
{ id: 1, name: 'Anna' },
{ id: 2, name: 'Oleh' },
];
}
}У цьому випадку LoggingInterceptor працює лише для маршруту GET /users.
Якщо декоратор розмістити над класом, interceptor застосовуватиметься до всіх його методів:
import {
Controller,
Get,
Post,
UseInterceptors,
} from '@nestjs/common';
import { LoggingInterceptor } from './logging.interceptor';
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {
@Get()
findAll() {
return [];
}
@Post()
create() {
return { id: 1 };
}
}Тепер interceptor працює для GET /users і POST /users.
Глобальний interceptor можна підключити в main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { LoggingInterceptor } from './logging.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new LoggingInterceptor());
await app.listen(3000);
}
bootstrap();Тоді interceptor застосовується до всіх маршрутів застосунку.
Нижче наведено приклад interceptor, який:
логуватиме метод і URL запиту;
вимірюватиме тривалість обробки;
додаватиме результат у властивість data.
response.interceptor.tsimport {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { map, Observable, tap } from 'rxjs';
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
private readonly logger = new Logger(ResponseInterceptor.name);
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
const request = context.switchToHttp().getRequest();
const { method, url } = request;
const startedAt = Date.now();
this.logger.log(`Початок: ${method} ${url}`);
return next.handle().pipe(
map((data) => ({
data,
})),
tap(() => {
const duration = Date.now() - startedAt;
this.logger.log(
`Завершення: ${method} ${url} за ${duration} мс`,
);
}),
);
}
}users.controller.tsimport {
Controller,
Get,
UseInterceptors,
} from '@nestjs/common';
import { ResponseInterceptor } from './response.interceptor';
@Controller('users')
@UseInterceptors(ResponseInterceptor)
export class UsersController {
@Get()
findAll() {
return [
{ id: 1, name: 'Anna' },
{ id: 2, name: 'Oleh' },
];
}
}app.module.tsimport { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
@Module({
controllers: [UsersController],
})
export class AppModule {}main.tsimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Після запуску запит:
GET http://localhost:3000/usersповерне:
{
"data": [
{
"id": 1,
"name": "Anna"
},
{
"id": 2,
"name": "Oleh"
}
]
}А в консолі з’являться повідомлення про початок і завершення запиту.
ExecutionContextExecutionContext містить інформацію про поточний тип виконання. Для HTTP-запиту зазвичай використовують:
const http = context.switchToHttp();
const request = http.getRequest();
const response = http.getResponse();Через request можна отримати:
HTTP-метод;
URL;
заголовки;
параметри;
тіло запиту;
поточного користувача, якщо його було додано раніше.
Наприклад:
const request = context.switchToHttp().getRequest();
console.log(request.method);
console.log(request.url);
console.log(request.headers);ExecutionContext також дає змогу отримати метадані поточного класу або методу:
const className = context.getClass().name;
const handlerName = context.getHandler().name;Це корисно, коли interceptor має працювати по-різному залежно від конкретного обробника.
Interceptor може виконати код у разі помилки за допомогою другого аргументу оператора tap:
import {
CallHandler,
ExecutionContext,
Injectable,
Logger,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class ErrorLoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(ErrorLoggingInterceptor.name);
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
return next.handle().pipe(
tap({
error: (error: Error) => {
this.logger.error(`Помилка: ${error.message}`);
},
}),
);
}
}Такий interceptor лише спостерігає за помилкою і записує її в журнал. Якщо interceptor має змінити помилку або перетворити її на іншу, для цього використовують оператори RxJS, наприклад catchError.
Важливо, що interceptor не повинен без потреби приховувати помилки. Якщо помилку перехопили, але не повернули або не викинули далі, можна змінити очікувану поведінку обробки запиту.
Interceptor може використовувати сервіси через dependency injection, як і інші класи NestJS:
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { MetricsService } from './metrics.service';
@Injectable()
export class MetricsInterceptor implements NestInterceptor {
constructor(private readonly metricsService: MetricsService) {}
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
this.metricsService.incrementRequests();
return next.handle();
}
}Якщо interceptor застосовується через @UseInterceptors(MetricsInterceptor), NestJS зможе створити його через контейнер залежностей, якщо відповідні залежності доступні в модулі.
Для глобального interceptor, якому потрібні залежності, зручно використовувати токен APP_INTERCEPTOR:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { ResponseInterceptor } from './response.interceptor';
import { UsersController } from './users.controller';
@Module({
controllers: [UsersController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ResponseInterceptor,
},
],
})
export class AppModule {}У такому випадку interceptor залишається керованим контейнером NestJS і може отримувати залежності через конструктор.
next.handle()Якщо interceptor не викликає next.handle() і не повертає інший Observable, обробка запиту може не продовжитися:
intercept(context: ExecutionContext, next: CallHandler) {
console.log('Запит отримано');
// Помилка: метод контролера не буде викликано
}Якщо interceptor не має зупиняти запит, він повинен повернути:
return next.handle();tap для зміни відповідіtap не змінює значення:
return next.handle().pipe(
tap((data) => {
return { data };
}),
);Повернуте з callback значення буде проігноровано. Для зміни відповіді використовуйте map:
return next.handle().pipe(
map((data) => ({
data,
})),
);Interceptor, підключений до одного методу, не впливає на інші методи контролера. Перевірте, де розміщено @UseInterceptors():
над методом — лише один маршрут;
над класом — усі маршрути контролера;
через useGlobalInterceptors або APP_INTERCEPTOR — увесь застосунок.
Такий interceptor створюється вручну:
app.useGlobalInterceptors(new ResponseInterceptor());Якщо його конструктору потрібен сервіс, NestJS не зможе автоматично передати цей сервіс у створений вручну екземпляр. Для interceptor із залежностями використовуйте реєстрацію через APP_INTERCEPTOR.
Interceptor має працювати із загальними аспектами обробки:
логуванням;
вимірюванням часу;
зміною єдиного формату відповіді;
обробкою загальних подій до або після запиту.
Бізнес-логіку конкретного маршруту краще залишати в сервісах і контролерах.
Interceptor виконує код до та після методу контролера.
next.handle() запускає наступний етап обробки й повертає Observable.
tap використовують для побічних дій, наприклад логування.
map дає змогу змінити результат обробника.
ExecutionContext надає доступ до HTTP-запиту та поточного обробника.
Interceptor можна застосувати до методу, контролера або всього застосунку.
Для interceptor із залежностями на глобальному рівні зручно використовувати APP_INTERCEPTOR.