Пошук уроків, статей та іншого контенту
Створите власний filter для обробки конкретного типу винятків і підключите його на потрібному рівні.
NestJS має стандартну обробку винятків. Наприклад, NotFoundException автоматично перетворюється на HTTP-відповідь зі статусом 404.
Власний exception filter потрібен, коли для конкретного типу винятку необхідно:
змінити формат відповіді;
додати власні поля;
записати виняток у журнал;
обробити виняток лише для певного контролера або методу.
Filter може бути прив’язаний до:
окремого методу контролера;
усього контролера;
усього застосунку.
Створимо виняток для ситуації, коли товар не знайдено:
import { NotFoundException } from '@nestjs/common';
export class ProductNotFoundException extends NotFoundException {
constructor(productId: number) {
super(`Товар з ідентифікатором ${productId} не знайдено`);
}
}Клас успадковується від NotFoundException, тому він уже має HTTP-статус 404.
Виняток можна викинути безпосередньо в методі контролера або сервісу:
throw new ProductNotFoundException(id);Filter повинен:
мати декоратор @Catch();
реалізувати інтерфейс ExceptionFilter;
містити метод catch();
отримати HTTP-контекст через ArgumentsHost.
import {
ArgumentsHost,
Catch,
ExceptionFilter,
} from '@nestjs/common';
import { Response } from 'express';
import { ProductNotFoundException } from './product-not-found.exception';
@Catch(ProductNotFoundException)
export class ProductNotFoundFilter
implements ExceptionFilter<ProductNotFoundException>
{
catch(
exception: ProductNotFoundException,
host: ArgumentsHost,
): void {
const context = host.switchToHttp();
const response = context.getResponse<Response>();
response.status(exception.getStatus()).json({
statusCode: exception.getStatus(),
error: 'ProductNotFound',
message: exception.message,
});
}
}@Catch()Аргументом @Catch() можна вказати конкретний клас винятку:
@Catch(ProductNotFoundException)Такий filter оброблятиме лише ProductNotFoundException.
Інші винятки передаватимуться стандартному механізму NestJS або іншому filter, якщо його підключено.
Якщо потрібно обробляти всі винятки, використовують декоратор без аргументів:
@Catch()Однак для конкретної бізнес-помилки краще вказувати конкретний тип. Так filter не перехоплюватиме помилки, для яких він не призначений.
Для підключення filter лише до одного методу використовується @UseFilters():
import {
Controller,
Get,
Param,
ParseIntPipe,
UseFilters,
} from '@nestjs/common';
@Controller('products')
export class ProductsController {
@Get(':id')
@UseFilters(ProductNotFoundFilter)
getProduct(
@Param('id', ParseIntPipe) id: number,
) {
if (id !== 1) {
throw new ProductNotFoundException(id);
}
return {
id: 1,
name: 'Keyboard',
};
}
}Тепер filter застосовується тільки до маршруту GET /products/:id.
Для запиту GET /products/1 контролер поверне товар:
{
"id": 1,
"name": "Keyboard"
}Для запиту GET /products/10 буде повернуто:
{
"statusCode": 404,
"error": "ProductNotFound",
"message": "Товар з ідентифікатором 10 не знайдено"
}Якщо однакова обробка потрібна для всіх методів контролера, @UseFilters() можна розмістити над класом:
import {
Controller,
Get,
UseFilters,
} from '@nestjs/common';
@Controller('products')
@UseFilters(ProductNotFoundFilter)
export class ProductsController {
@Get(':id')
getProduct() {
throw new ProductNotFoundException(10);
}
@Get(':id/reviews')
getReviews() {
throw new ProductNotFoundException(10);
}
}У цьому випадку filter працюватиме для всіх маршрутів ProductsController, але лише коли виникає ProductNotFoundException.
Нижче наведено мінімальний приклад застосунку, у якому filter підключений до методу контролера:
import {
ArgumentsHost,
Catch,
Controller,
ExceptionFilter,
Get,
Injectable,
Module,
NotFoundException,
Param,
ParseIntPipe,
Response,
UseFilters,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import type { Response as ExpressResponse } from 'express';
export class ProductNotFoundException extends NotFoundException {
constructor(productId: number) {
super(`Товар з ідентифікатором ${productId} не знайдено`);
}
}
@Catch(ProductNotFoundException)
export class ProductNotFoundFilter
implements ExceptionFilter<ProductNotFoundException>
{
catch(
exception: ProductNotFoundException,
host: ArgumentsHost,
): void {
const context = host.switchToHttp();
const response = context.getResponse<ExpressResponse>();
response.status(exception.getStatus()).json({
statusCode: exception.getStatus(),
error: 'ProductNotFound',
message: exception.message,
});
}
}
@Injectable()
export class ProductsService {
findById(id: number) {
if (id !== 1) {
throw new ProductNotFoundException(id);
}
return {
id: 1,
name: 'Keyboard',
};
}
}
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Get(':id')
@UseFilters(ProductNotFoundFilter)
getProduct(@Param('id', ParseIntPipe) id: number) {
return this.productsService.findById(id);
}
}
@Module({
controllers: [ProductsController],
providers: [ProductsService],
})
export class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Після запуску:
GET /products/1 поверне знайдений товар;
GET /products/2 викличе ProductNotFoundException;
ProductNotFoundFilter перетворить виняток на власний JSON-формат.
Якщо filter має обробляти виняток у всьому застосунку, його можна підключити в main.ts:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ProductNotFoundFilter } from './product-not-found.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new ProductNotFoundFilter());
await app.listen(3000);
}
bootstrap();Тоді ProductNotFoundFilter буде доступним для всіх контролерів і маршрутів.
Глобальне підключення варто використовувати лише тоді, коли однакова обробка справді потрібна в усьому застосунку. Для локальної поведінки краще використовувати рівень методу або контролера.
@UseFilters(ProductNotFoundFilter)
@Get(':id')
getProduct() {
// ...
}Використовуйте, коли filter потрібен лише для одного маршруту.
@Controller('products')
@UseFilters(ProductNotFoundFilter)
export class ProductsController {
// ...
}Використовуйте, коли filter потрібен усім методам одного контролера.
app.useGlobalFilters(new ProductNotFoundFilter());Використовуйте, коли filter має працювати для всього застосунку.
Якщо filter оголошено так:
@Catch(ProductNotFoundException)він не оброблятиме звичайний NotFoundException:
throw new NotFoundException('Товар не знайдено');У такому разі потрібно або викидати ProductNotFoundException, або змінити тип у @Catch().
response.status()Якщо викликати лише response.json(), сервер може повернути статус 200, хоча сталася помилка:
response.json({
message: exception.message,
});Правильний варіант:
response.status(exception.getStatus()).json({
message: exception.message,
});Filter, підключений до конкретного методу, не впливатиме на інші маршрути контролера. Якщо потрібно обробляти виняток у всьому контролері, перенесіть @UseFilters() на рівень класу.
Якщо виняток успадковується від HttpException, не потрібно дублювати статус вручну:
response.status(exception.getStatus()).json({
// ...
});Так filter використає статус, визначений самим винятком.
Власний exception filter реалізує інтерфейс ExceptionFilter.
Декоратор @Catch(ProductNotFoundException) обмежує filter конкретним типом винятку.
Через ArgumentsHost можна отримати HTTP-запит і відповідь.
@UseFilters() підключає filter до методу або контролера.
app.useGlobalFilters() підключає filter до всього застосунку.
Для коректної відповіді потрібно встановити HTTP-статус і повернути JSON через response.status(...).json(...).