Чому?
Перше питання: чому нам потрібно валідувати запити та відповіді?
Переваги валідації запитів та відповідей є суттєвими:
- Захист від шкідливих даних і несанкціонованого витоку даних
- Чіткі контракти DTO між вашим API на NestJS та його споживачами
- Перезавантажувана логіка трансформації та мапінгу даних через DTO
- Покращена типова безпека та краща підтримка IDE
Але є й можливі недоліки:
- Не запобігає повністю ін’єкціям даних і витоку даних
- Складність початкової налаштування, особливо для команд, які не знайомі з декораторами та патернами валідації
- Потенційні труднощі з версіонуванням при оновленні DTO в розподілених системах
- Повільніший час відповіді при складній валідації
Я особисто рекомендую цей підхід, оскільки вважаю, що переваги значно переважують недоліки. Крім того, це чудовий перший крок до кращого розділення відповідальностей (SoC)
Валідація запитів
Для реалізації валідації в NestJS нам потрібно буде встановити пакет валідації. Хоча є кілька варіантів, таких як Zod, typebox, Superstrucjs або Effect-ts Schema, ми будемо використовувати офіційно рекомендоване рішення class-validator та class-transformer.
Давайте встановимо їх:
npm i --save class-validator class-transformer
Далі, в нашому main.ts ми можемо використати вбудований в Nest ValidationPipe для автоматичної валідації всього додатку.
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
Щоб повідомити ValidationPipe, як потрібно валідувати наші дані, нам треба створити деякі DTO.
// cat.dto.ts
import { IsString, IsNumber } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsNumber()
age: number;
}
Хоча ми використовуємо термін "DTO", це здебільшого є конвенцією.
Ці класи визначають очікувану структуру та правила валідації для вхідних даних запиту, що відповідає меті шаблону DTO для передачі даних та валідації.
Наприкінці, у нашому контролері ми можемо застосувати DTO до відповідного маршруту.
// cat.controller.ts
@Controller('cat')
export class CatController {
@Post()
create(@Body() createCatDto: CreateCatDto) {
return 'create cat';
}
}
Не забувайте імпортувати контролер у ваш app.module.ts, щоб зробити його доступним.
Тепер, якщо ми зробимо POST запит на “http://localhost:3000/cat”, ви повинні побачити повідомлення про помилку, наприклад:
{
"message": [
"name must be a string",
"age must be a number conforming to the specified constraints"
],
"error": "Bad Request",
"statusCode": 400
}
Помилка валідації надає чіткий зворотний зв’язок про причину, чому запит був відхилений через масив повідомлень.
Хоча ці детальні повідомлення про помилки корисні під час розробки, вони повинні бути вимкнені в продуктивному середовищі, щоб уникнути можливих вразливостей безпеки через розголошення інформації через помилки.
Для цього ми можемо додати конфігурацію в наш ValidationPipe.
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
disableErrorMessages: true, // вимкнути об'єкт повідомлення
}),
);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
Для додаткової безпеки ми можемо увімкнути опцію whitelist, яка автоматично видаляє всі властивості з запиту, які не визначені явно в нашому DTO.
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
disableErrorMessages: true,
whitelist: true,
}),
);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
Тепер, навіть якщо користувач відправить більше, ніж очікуваний об'єкт, наприклад:
{
"name": "aya",
"age": 3,
"gender": "female" // додаткове значення
}
"createCatDto" після валідації в контролері отримає лише:
{
"name": "aya",
"age": 3,
}
Більше конфігурацій можна знайти в офіційній документації тут разом з прикладами.
Валідація відповіді
Валідація відповіді є важливою для підтримки послідовності контрактів API та запобігання витокам чутливих даних. Хоча NestJS не надає вбудованої валідації відповіді, ми можемо реалізувати надійне рішення, використовуючи перехоплювачі.
Давайте реалізуємо власний перехоплювач для валідації наших відповідей.
// response-validation.interceptor.ts
import {
CallHandler,
ExecutionContext,
Injectable,
InternalServerErrorException,
NestInterceptor,
} from '@nestjs/common';
import { instanceToPlain, plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { Observable, switchMap } from 'rxjs';
@Injectable()
export class ResponseValidationInterceptor
implements NestInterceptor
{
constructor(private readonly dto: new () => T) {}
intercept(context: ExecutionContext, next: CallHandler): Observable {
return next.handle().pipe(
switchMap(async (data) => {
const transformedData = plainToInstance(
this.dto,
instanceToPlain(data),
);
const errors = await validate(transformedData);
if (errors.length > 0) {
throw new InternalServerErrorException({
message: 'Response validation failed',
errors,
});
}
return transformedData;
}),
);
}
}
Ось як працює перехоплювач:
- Перехоплювач приймає клас DTO через свій конструктор. Цей DTO визначає очікувану структуру та правила валідації для ваших даних відповіді.
2.
Коли ваш кінцевий пункт (endpoint) повертає дані, перехоплювач перехоплює їх перед тим, як вони досягнуть клієнта. Він використовує операторswitchMap
з бібліотеки RxJS для обробки асинхронного процесу валідації. - Перехоплювач перетворює ваші дані відповіді на екземпляр вашого DTO за допомогою
plainToInstance
. Це гарантує, що всі декоратори класу класуclass-transformer
будуть правильно застосовані. - Потім він валідує перетворені дані відповідно до правил валідації вашого DTO, використовуючи функцію
validate
з бібліотекиclass-validator
.
Давайте подивимося на приклад:
// cat.dto.ts
export class GetCatDto {
@IsString()
name: string;
@IsNumber()
age: number;
}
// cat.controller.ts
@Get()
@UseInterceptors(new ResponseValidationInterceptor(GetCatDto))
findAll(): GetCatDto {
// Це викличе помилку валідації, оскільки age — це рядок
const wrongCat = {
name: 'wrongCat',
age: '1', // Має бути числом згідно з нашим DTO
};
return wrongCat;
}
Коли цей кінцевий пункт буде викликаний, перехоплювач зловить помилку валідації і поверне:
{
"message": "Response validation failed",
"errors": [
{
"target": {
"name": "wrongCat",
"age": "1"
},
"value": "1",
"property": "age",
"children": [],
"constraints": {
"isNumber": "age must be a number conforming to the specified constraints"
}
}
]
}
Знову ж таки, з міркувань безпеки я рекомендую уникати розкриття детальних помилок валідації у вашому продуктивному середовищі. Замість цього, логуйте ці помилки всередині і повертайте загальну помилку 500 клієнту. Це допоможе запобігти потенційному витоку інформації про внутрішню структуру вашого API.
З цим перехоплювачем ви можете забезпечити, що ваші відповіді API завжди відповідають вашій бажаній структурі даних та правилам валідації, що додає додатковий рівень безпеки та надійності вашому додатку.
Ресурси
Контакти
Якщо у вас є питання або виявлені баги, напишіть мені в коментарях!
Ви також можете зв'язатися зі мною через:
Перекладено з: Nestjs Request/Response Validation