Пошук уроків, статей та іншого контенту
Налаштуєте перетворення об’єктів відповідей і приховування внутрішніх або чутливих полів.
Серіалізація — це перетворення об’єкта, який повертає обробник маршруту, у формат відповіді клієнту, зазвичай JSON.
У NestJS для цього використовується ClassSerializerInterceptor, який інтегрується з бібліотекою class-transformer. За допомогою декораторів можна:
приховати приватні поля;
перейменувати поля у відповіді;
перетворити значення перед відправленням;
керувати серіалізацією вкладених об’єктів.
Це особливо важливо, коли об’єкт містить дані, які не повинні потрапляти до клієнта: пароль, службові прапорці, токени або внутрішні ідентифікатори.
Серіалізація відповідей не замінює перевірку вхідних даних. Вона відповідає саме за підготовку даних, які сервер відправляє клієнту.
ClassSerializerInterceptorСпочатку переконайтеся, що встановлена бібліотека class-transformer:
npm install class-transformerІнтерцептор можна підключити глобально у файлі main.ts:
import { NestFactory } from '@nestjs/core';
import { Reflector } from '@nestjs/core';
import { ClassSerializerInterceptor } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(
new ClassSerializerInterceptor(app.get(Reflector)),
);
await app.listen(3000);
}
bootstrap();Після цього NestJS застосовуватиме серіалізацію до відповідей усіх контролерів.
Також інтерцептор можна підключити лише до конкретного контролера або маршруту:
import {
ClassSerializerInterceptor,
Controller,
Get,
UseInterceptors,
} from '@nestjs/common';
@Controller('users')
@UseInterceptors(ClassSerializerInterceptor)
export class UsersController {
@Get('profile')
getProfile() {
return this.getUser();
}
private getUser() {
return {};
}
}Глобальний варіант зручний, якщо застосунок послідовно використовує класи для представлення відповідей. Локальний варіант підходить, коли серіалізація потрібна лише для окремих частин API.
@ExcludeРозглянемо клас користувача, який містить пароль та внутрішнє поле internalNote:
import { Exclude } from 'class-transformer';
export class UserResponse {
id: number;
email: string;
displayName: string;
@Exclude()
password: string;
@Exclude()
internalNote: string;
constructor(partial: Partial<UserResponse>) {
Object.assign(this, partial);
}
}Тепер контролер може повернути екземпляр цього класу:
import { Controller, Get } from '@nestjs/common';
import { UserResponse } from './user-response';
@Controller('users')
export class UsersController {
@Get('me')
getCurrentUser(): UserResponse {
return new UserResponse({
id: 1,
email: 'olena@example.com',
displayName: 'Олена',
password: 'very-secret-password',
internalNote: 'Користувач створений вручну',
});
}
}Клієнт отримає:
{
"id": 1,
"email": "olena@example.com",
"displayName": "Олена"
}Поля password та internalNote залишаться в об’єкті на сервері, але не потраплять у JSON-відповідь.
Декоратори class-transformer працюють з екземплярами класів. Тому потрібно повертати:
return new UserResponse(user);а не звичайний об’єкт:
return {
id: user.id,
email: user.email,
password: user.password,
};Звичайний об’єкт не має метаданих класу, тому декоратор @Exclude() може не застосуватися.
excludeAllУ попередньому прикладі всі поля серіалізуються за замовчуванням, а окремі поля виключаються через @Exclude().
Для чутливих відповідей часто безпечніше використовувати протилежну стратегію: виключити все, а явно дозволити лише потрібні поля за допомогою @Expose().
import { Exclude, Expose } from 'class-transformer';
@Exclude()
export class UserResponse {
@Expose()
id: number;
@Expose()
email: string;
@Expose()
displayName: string;
password: string;
internalNote: string;
constructor(partial: Partial<UserResponse>) {
Object.assign(this, partial);
}
}Коли клас має @Exclude() на рівні класу, усі властивості виключені за замовчуванням. У відповідь потрапляють лише властивості з @Expose().
Такий підхід зменшує ризик випадково відкрити нове поле після зміни класу:
@Exclude()
export class UserResponse {
@Expose()
id: number;
@Expose()
email: string;
// Нове поле не буде відправлене клієнту,
// доки для нього явно не додадуть @Expose().
internalRole: string;
}Декоратор @Expose() може задати інше ім’я поля у JSON:
import { Exclude, Expose } from 'class-transformer';
@Exclude()
export class UserResponse {
@Expose()
id: number;
@Expose({ name: 'fullName' })
displayName: string;
constructor(partial: Partial<UserResponse>) {
Object.assign(this, partial);
}
}Внутрішньо властивість називається displayName, але відповідь матиме такий вигляд:
{
"id": 1,
"fullName": "Олена"
}Це корисно, коли внутрішня модель або назви полів бази даних не повинні бути частиною публічного API.
@Transform@Transform() дає змогу змінити значення перед серіалізацією.
Наприклад, дату можна явно перетворити на ISO-рядок:
import { Exclude, Expose, Transform } from 'class-transformer';
@Exclude()
export class UserResponse {
@Expose()
id: number;
@Expose()
email: string;
@Expose()
@Transform(({ value }) => value.toISOString(), { toPlainOnly: true })
createdAt: Date;
@Exclude()
password: string;
constructor(partial: Partial<UserResponse>) {
Object.assign(this, partial);
}
}Параметр toPlainOnly: true означає, що перетворення застосовується під час підготовки об’єкта до звичайного JSON-представлення, але не під час створення екземпляра класу.
Приклад відповіді:
{
"id": 1,
"email": "olena@example.com",
"createdAt": "2026-09-01T10:30:00.000Z"
}У цьому прикладі createdAt на сервері залишається об’єктом Date, але клієнт отримує рядок.
Для вкладених об’єктів варто описати їх окремим класом і вказати тип через @Type():
import { Exclude, Expose, Type } from 'class-transformer';
@Exclude()
export class ProfileResponse {
@Expose()
firstName: string;
@Expose()
lastName: string;
constructor(partial: Partial<ProfileResponse>) {
Object.assign(this, partial);
}
}
@Exclude()
export class UserResponse {
@Expose()
id: number;
@Expose()
email: string;
@Expose()
@Type(() => ProfileResponse)
profile: ProfileResponse;
@Exclude()
password: string;
constructor(partial: Partial<UserResponse>) {
Object.assign(this, partial);
}
}Контролер:
import { Controller, Get } from '@nestjs/common';
import { ProfileResponse, UserResponse } from './user-response';
@Controller('users')
export class UsersController {
@Get('me')
getCurrentUser(): UserResponse {
const profile = new ProfileResponse({
firstName: 'Олена',
lastName: 'Коваль',
});
return new UserResponse({
id: 1,
email: 'olena@example.com',
password: 'very-secret-password',
profile,
});
}
}Відповідь:
{
"id": 1,
"email": "olena@example.com",
"profile": {
"firstName": "Олена",
"lastName": "Коваль"
}
}Якщо вкладений об’єкт також містить чутливі поля, його клас має самостійно описувати правила серіалізації.
Якщо глобальний інтерцептор не використовується, його можна додати до конкретного маршруту:
import {
ClassSerializerInterceptor,
Controller,
Get,
UseInterceptors,
} from '@nestjs/common';
@Controller('users')
export class UsersController {
@Get('me')
@UseInterceptors(ClassSerializerInterceptor)
getCurrentUser() {
return new UserResponse({
id: 1,
email: 'olena@example.com',
password: 'very-secret-password',
});
}
}Інтерцептор застосує правила класу UserResponse лише до цього маршруту.
Нижче наведено повний приклад класу відповіді та контролера:
// user-response.ts
import { Exclude, Expose, Transform } from 'class-transformer';
@Exclude()
export class UserResponse {
@Expose()
id: number;
@Expose()
email: string;
@Expose({ name: 'name' })
displayName: string;
@Expose()
@Transform(({ value }) => value.toISOString(), { toPlainOnly: true })
createdAt: Date;
password: string;
refreshToken: string;
internalNote: string;
constructor(partial: Partial<UserResponse>) {
Object.assign(this, partial);
}
}// users.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UserResponse } from './user-response';
@Controller('users')
export class UsersController {
@Get('me')
getCurrentUser(): UserResponse {
return new UserResponse({
id: 42,
email: 'olena@example.com',
displayName: 'Олена Коваль',
createdAt: new Date('2026-09-01T10:30:00.000Z'),
password: 'password-from-database',
refreshToken: 'refresh-token-from-database',
internalNote: 'Внутрішня службова інформація',
});
}
}За умови глобально підключеного ClassSerializerInterceptor клієнт отримає:
{
"id": 42,
"email": "olena@example.com",
"name": "Олена Коваль",
"createdAt": "2026-09-01T10:30:00.000Z"
}return {
id: user.id,
email: user.email,
password: user.password,
};У такому випадку правила класу відповіді не використовуються. Створюйте екземпляр:
return new UserResponse(user);ClassSerializerInterceptorДекоратори @Exclude(), @Expose() та @Transform() самі по собі не змінюють відповідь NestJS. Потрібно підключити інтерцептор глобально або до конкретного маршруту.
Сутність може містити набагато більше даних, ніж потрібно клієнту. Якщо повертати її безпосередньо, можна випадково відкрити пароль, службові поля або внутрішню структуру системи.
Краще створювати окремий клас відповіді, який описує публічний формат API.
Якщо використовувати звичайні класи з кількома @Exclude(), нове поле автоматично може потрапити у відповідь. Для особливо чутливих відповідей безпечніше використовувати @Exclude() на рівні класу та явно додавати дозволені поля через @Expose().
Серіалізація змінює лише представлення відповіді. Вона не видаляє поле з об’єкта в пам’яті та не змінює запис у базі даних.
ClassSerializerInterceptor вмикає серіалізацію класів у NestJS.
@Exclude() приховує властивість у відповіді.
@Expose() явно дозволяє властивість і може змінювати її публічне ім’я.
@Transform() перетворює значення перед відправленням клієнту.
@Type() допомагає описувати вкладені об’єкти.
Для застосування декораторів потрібно повертати екземпляр класу, а не звичайний об’єкт.
Для безпечних відповідей зручно виключати всі поля за замовчуванням і явно відкривати лише необхідні.