Пошук уроків, статей та іншого контенту
Перетворите відповіді контролерів за допомогою RxJS і interceptor, зберігаючи єдиний формат API.
У різних контролерах API можуть повертати дані в різних форматах:
[
{ "id": 1, "name": "Anna" }
]{
"id": 1,
"name": "Anna"
}Для клієнта зручніше, коли всі успішні відповіді мають однакову структуру. Наприклад:
{
"data": [
{ "id": 1, "name": "Anna" }
]
}Або:
{
"data": {
"id": 1,
"name": "Anna"
}
}У NestJS для цього можна використати interceptor. Він перехоплює виконання методу контролера, отримує його результат і змінює відповідь перед відправленням клієнту.
Interceptor має доступ до:
контексту виконання через ExecutionContext;
наступного елемента ланцюжка через CallHandler.
Метод next.handle() запускає контролер і повертає Observable. Оскільки результат контролера представлений як RxJS-потік, його можна перетворити оператором map.
Загальна схема:
запит
↓
interceptor
↓
контролер
↓
Observable з результатом
↓
map(...)
↓
трансформована відповідьБазова структура interceptor:
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<unknown> {
return next.handle().pipe(
map((data) => ({
data,
})),
);
}Тут:
next.handle() отримує результат контролера.
pipe() створює ланцюжок RxJS-операторів.
map() обгортає результат у властивість data.
Новий об'єкт стає відповіддю API.
Створимо файл transform.interceptor.ts:
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
export interface ApiResponse<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, ApiResponse<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => ({
data,
})),
);
}
}NestInterceptor<T, R> приймає два узагальнені типи:
T — тип початкового результату контролера;
R — тип результату після трансформації.
У цьому прикладі:
NestInterceptor<T, ApiResponse<T>>означає, що контролер може повернути значення типу T, а клієнт отримає об'єкт такого формату:
{
data: T
}Interceptor можна застосувати до конкретного контролера за допомогою @UseInterceptors().
import {
Controller,
Get,
UseInterceptors,
} from '@nestjs/common';
import { TransformInterceptor } from './transform.interceptor';
interface User {
id: number;
name: string;
}
@Controller('users')
@UseInterceptors(TransformInterceptor)
export class UsersController {
@Get()
findAll(): User[] {
return [
{ id: 1, name: 'Anna' },
{ id: 2, name: 'Oleh' },
];
}
@Get('first')
findFirst(): User {
return {
id: 1,
name: 'Anna',
};
}
}Без interceptor запити повертали б такі результати:
[
{
"id": 1,
"name": "Anna"
},
{
"id": 2,
"name": "Oleh"
}
]{
"id": 1,
"name": "Anna"
}Після застосування interceptor формат буде єдиним:
{
"data": [
{
"id": 1,
"name": "Anna"
},
{
"id": 2,
"name": "Oleh"
}
]
}{
"data": {
"id": 1,
"name": "Anna"
}
}Нижче наведено мінімальний приклад із контролером, interceptor і модулем.
transform.interceptor.tsimport {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
export interface ApiResponse<T> {
data: T;
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, ApiResponse<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<ApiResponse<T>> {
return next.handle().pipe(
map((data) => ({
data,
})),
);
}
}users.controller.tsimport {
Controller,
Get,
UseInterceptors,
} from '@nestjs/common';
import { TransformInterceptor } from './transform.interceptor';
interface User {
id: number;
name: string;
}
@Controller('users')
@UseInterceptors(TransformInterceptor)
export class UsersController {
@Get()
findAll(): User[] {
return [
{ id: 1, name: 'Anna' },
{ id: 2, name: 'Oleh' },
];
}
@Get(':id')
findOne(): User {
return {
id: 1,
name: 'Anna',
};
}
}app.module.tsimport { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
@Module({
controllers: [UsersController],
})
export class AppModule {}Після запуску застосунку запит:
GET /usersповерне:
{
"data": [
{
"id": 1,
"name": "Anna"
},
{
"id": 2,
"name": "Oleh"
}
]
}Якщо єдиний формат потрібен для всього API, interceptor можна зареєструвати глобально.
main.tsimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './transform.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new TransformInterceptor());
await app.listen(3000);
}
bootstrap();Тепер interceptor застосовуватиметься до всіх контролерів застосунку.
APP_INTERCEPTORРеєстрація через APP_INTERCEPTOR виконується контейнером залежностей NestJS:
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { UsersController } from './users.controller';
import { TransformInterceptor } from './transform.interceptor';
@Module({
controllers: [UsersController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: TransformInterceptor,
},
],
})
export class AppModule {}Такий підхід зручний, якщо interceptor має залежності, які потрібно отримувати через dependency injection.
Єдиний формат відповіді може містити не лише data, а й метадані, наприклад повідомлення або час виконання.
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
@Injectable()
export class ResponseInterceptor<T>
implements NestInterceptor<T, object>
{
intercept(
context: ExecutionContext,
next: CallHandler<T>,
): Observable<object> {
return next.handle().pipe(
map((data) => ({
success: true,
data,
})),
);
}
}Результат контролера:
{
id: 1,
name: 'Anna',
}Після трансформації:
{
"success": true,
"data": {
"id": 1,
"name": "Anna"
}
}Структуру відповіді потрібно заздалегідь узгодити з клієнтами API. Не варто без потреби змінювати формат після того, як API вже використовується.
Interceptor однаково обробляє різні результати контролера:
return [];перетвориться на:
{
"data": []
}А результат:
return null;перетвориться на:
{
"data": null
}Це означає, що interceptor не змінює саме значення, а лише додає до нього узгоджену оболонку.
Оператор map виконується для успішного результату next.handle(). Якщо контролер або сервіс викидає виняток, успішна трансформація відповіді не виконується.
Наприклад:
import {
Controller,
Get,
NotFoundException,
} from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get('missing')
findMissing() {
throw new NotFoundException('Користувача не знайдено');
}
}Такий запит обробляється механізмом помилок NestJS, а не map в interceptor. Для помилок потрібні exception filters або стандартний формат помилок, узгоджений окремо.
return next.handle()Interceptor повинен повертати Observable:
intercept(context: ExecutionContext, next: CallHandler) {
return next.handle().pipe(
map((data) => ({ data })),
);
}Якщо не повернути результат next.handle(), ланцюжок виконання не отримає відповідь контролера.
await замість RxJS-ланцюжкаnext.handle() повертає Observable, а не звичайний Promise. Для перетворення результату використовуйте pipe та map:
return next.handle().pipe(
map((data) => ({
data,
})),
);Якщо контролер уже повертає:
return {
data: users,
};а interceptor додає data ще раз, клієнт отримає:
{
"data": {
"data": []
}
}Потрібно обрати одне місце для формування оболонки відповіді: або контролери, або interceptor.
Якщо interceptor додано лише до частини контролерів, API може мати різні формати відповідей. Для єдиного формату застосовуйте його глобально або до всіх потрібних контролерів.
mapmap призначений для перетворення успішних значень. Не слід очікувати, що він автоматично змінить формат винятків, які виникають у контролері.
Interceptor перехоплює результат виконання контролера.
next.handle() повертає результат як RxJS Observable.
Оператор map дає змогу змінити формат успішної відповіді.
Обгортка { data } допомагає підтримувати єдиний формат API.
Interceptor можна застосувати до контролера або зареєструвати глобально.
Не слід обгортати відповідь одночасно в контролері та interceptor.
Помилки обробляються окремим механізмом і не трансформуються звичайним map.