Kurs NestJS · Moduł 2: Routing i cykl żądania
Filtry wyjątków - sądy i trybunały imperium
W tej lekcji6
Prowincjał pyta o trybut, którego nie ma w rejestrze, a serwer odpowiada kodem 500 i komunikatem „Internal server error". Klient nie wie, czy popełnił błąd, czy padła baza, a Ty nie wiesz, co mu właściwie wysłałeś. Pretor Augustus, najwyższy sędzia Rzymu, powtarza, że każda sprawa musi trafić przed właściwy trybunał i zakończyć się czytelnym wyrokiem. W NestJS tymi trybunałami są exception filters - klasy, które przechwytują wyjątki i zamieniają je w odpowiedzi HTTP.
Czym są Exception Filters?
W imperium rzymskim każdy rodzaj sprawy miał swój trybunał:
- Trybunał pretorski - sprawy cywilne (404 Not Found)
- Sąd cenzorski - kontrola dostępu (403 Forbidden)
- Trybunał wojskowy - poważne naruszenia (500 Internal Error)
NestJS ma wbudowaną warstwę wyjątków, która działa jak domyślny trybunał: przechwytuje każdy nieobsłużony wyjątek. Własny filtr piszesz wtedy, gdy chcesz zmienić treść wyroku.
Wbudowane wyjątki NestJS
Zanim napiszesz własny sąd, poznaj gotowe wyroki. Wszystkie poniższe klasy dziedziczą po HttpException i same ustawiają właściwy kod statusu:
1import {
2 BadRequestException, // 400
3 UnauthorizedException, // 401
4 ForbiddenException, // 403
5 NotFoundException, // 404
6 ConflictException, // 409
7 InternalServerErrorException, // 500
8 HttpException, // bazowy wyjątek HTTP
9} from '@nestjs/common';Komentarze przy importach podają kod, który trafi do klienta. Teraz rzucamy te wyjątki w kontrolerze trybutów:
1@Controller('tributa')
2export class TributeController {
3 private tributes = [
4 { id: 1, name: 'Aurum Galliae', amount: 50000 },
5 { id: 2, name: 'Argentum Hispaniae', amount: 30000 },
6 ];
7
8 @Get(':id')
9 findTribute(@Param('id', ParseIntPipe) id: number) {
10 const tribute = this.tributes.find(t => t.id === id);
11 if (!tribute) {
12 // Rzuć wyjątek 404
13 throw new NotFoundException(
14 `Tributum ${id} non inventum! (Trybut nie znaleziony)`
15 );
16 }
17 return tribute;
18 }
19
20 @Post()
21 @UseGuards(RolesGuard)
22 createTribute(@Body() dto: any) {
23 if (!dto.name || !dto.amount) {
24 // Rzuć wyjątek 400
25 throw new BadRequestException(
26 'Trybut musi zawierać nazwę i kwotę!'
27 );
28 }
29
30 const exists = this.tributes.find(t => t.name === dto.name);
31 if (exists) {
32 // Rzuć wyjątek 409
33 throw new ConflictException(
34 `Trybut o nazwie "${dto.name}" już istnieje!`
35 );
36 }
37
38 const newTribute = { id: this.tributes.length + 1, ...dto };
39 this.tributes.push(newTribute);
40 return newTribute;
41 }
42}Słowo throw przerywa metodę, a warstwa wyjątków NestJS wysyła odpowiedź za Ciebie. Dla brakującego trybutu klient dostanie taki JSON:
1{
2 "message": "Tributum 3 non inventum! (Trybut nie znaleziony)",
3 "error": "Not Found",
4 "statusCode": 404
5}Twój tekst trafia do pola message, a error i statusCode wynikają z klasy wyjątku. Gdy potrzebujesz kodu bez własnej klasy, sięgnij po bazowe HttpException z wartością z enuma HttpStatus:
1throw new HttpException('Wiadomość błędu', HttpStatus.I_AM_A_TEAPOT); // 418Kod 418 istnieje naprawdę, choć urodził się jako żart. Uwaga na zwykły throw new Error(): taki wyjątek nie jest HttpException, więc klient zobaczy tylko { "statusCode": 500, "message": "Internal server error" }. Szczegóły błędu zostają na serwerze - i dobrze.
Tworzenie własnych filtrów wyjątków
Własny format wyroku zapewnia klasa implementująca interfejs ExceptionFilter. Dekorator @Catch(HttpException) mówi, jakie sprawy trafiają do tego sądu, a metoda catch() dostaje wyjątek oraz ArgumentsHost - obiekt, z którego wyciągasz request i response:
1// filters/roman-exception.filter.ts
2import {
3 ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
4} from '@nestjs/common';
5import { Request, Response } from 'express';
6
7@Catch(HttpException)
8export class RomanExceptionFilter implements ExceptionFilter {
9 catch(exception: HttpException, host: ArgumentsHost) {
10 const ctx = host.switchToHttp();
11 const response = ctx.getResponse<Response>();
12 const request = ctx.getRequest<Request>();
13 const status = exception.getStatus();
14
15 // Własny format odpowiedzi błędu w stylu imperium
16 response.status(status).json({
17 statusCode: status,
18 imperium: 'Roma Aeterna',
19 error: exception.message,
20 path: request.url,
21 method: request.method,
22 timestamp: new Date().toISOString(),
23 });
24 }
25}Najpierw host.switchToHttp(), potem z kontekstu response i request, a na końcu wysłanie odpowiedzi. Status możesz odczytać z wyjątku (exception.getStatus()) w dowolnym miejscu przed wysłaniem. Ten filtr zmienia tylko kształt odpowiedzi - kod statusu przepisuje bez zmian z wyjątku. Jedna pułapka: przy błędach walidacji exception.message to ogólne „Bad Request Exception", a lista błędów pól leży w exception.getResponse().
Bindowanie filtrów - poziomy trybunału
Filtr podpinasz dekoratorem @UseFilters() do metody lub kontrolera albo globalnie dla całej aplikacji:
1// 1. Poziom metody - tylko ta metoda
2@Controller('tributa')
3export class TributeController {
4 @Get(':id')
5 @UseFilters(new RomanExceptionFilter()) // Tylko dla tej metody
6 findTribute(@Param('id') id: string) {
7 throw new NotFoundException('Trybut nie znaleziony!');
8 }
9}
10
11// 2. Poziom kontrolera - cały kontroler
12@Controller('legiones')
13@UseFilters(RomanExceptionFilter) // Cały kontroler
14export class LegionController {
15 @Get(':id')
16 findLegion(@Param('id') id: string) {
17 throw new NotFoundException('Legion nie znaleziony!');
18 }
19}
20
21// 3. Poziom globalny - cała aplikacja (w main.ts)
22// app.useGlobalFilters(new RomanExceptionFilter());Możesz przekazać instancję (new RomanExceptionFilter()) albo samą klasę. Dokumentacja NestJS zaleca klasę, bo framework może wtedy używać jednej instancji w całym module i wstrzykiwać jej zależności. app.useGlobalFilters() ma dwa ograniczenia: filtr powstaje poza systemem modułów, więc nie dostanie zależności przez konstruktor, i nie obejmuje gatewayów WebSocket. Dlatego globalny filtr wolę rejestrować jako provider:
1// app.module.ts - filtr globalny zarejestrowany jako provider
2import { Module } from '@nestjs/common';
3import { APP_FILTER } from '@nestjs/core';
4import { RomanExceptionFilter } from './filters/roman-exception.filter';
5
6@Module({
7 providers: [{ provide: APP_FILTER, useClass: RomanExceptionFilter }],
8})
9export class AppModule {}Działa tak samo jak useGlobalFilters, ale filtr może teraz wstrzyknąć na przykład logger. To polecam.
Filtr łapiący wszystko - najwyższy trybunał
Filtr z pustym @Catch() przechwytuje wszystko, także błędy, które nie są HttpException, na przykład zerwane połączenie z bazą:
1// filters/all-exceptions.filter.ts
2import {
3 ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus,
4} from '@nestjs/common';
5
6@Catch() // Bez argumentu = łapie WSZYSTKO
7export class AllExceptionsFilter implements ExceptionFilter {
8 catch(exception: unknown, host: ArgumentsHost) {
9 const ctx = host.switchToHttp();
10 const response = ctx.getResponse();
11 const request = ctx.getRequest();
12
13 const status = exception instanceof HttpException
14 ? exception.getStatus()
15 : HttpStatus.INTERNAL_SERVER_ERROR;
16
17 const message = exception instanceof HttpException
18 ? exception.message
19 : 'Błąd wewnętrzny serwera imperium!';
20
21 response.status(status).json({
22 statusCode: status,
23 error: message,
24 path: request.url,
25 timestamp: new Date().toISOString(),
26 });
27 }
28}Operator instanceof rozdziela dwie ścieżki: wyjątki HTTP zachowują swój status i komunikat, a cała reszta dostaje 500 i ogólny tekst. Nie odsyłaj klientowi exception.message z nieznanego błędu - mógłby zdradzić szczegóły bazy. Taki błąd zapisz w logach. Jeden filtr może też obsłużyć kilka typów naraz: @Catch(HttpException, TypeError).
Kolejność wykonania filtrów
Filtry są jedynym elementem cyklu żądania, który nie zaczyna od poziomu globalnego:
- Filtr metody (jeśli istnieje) - sprawdzany jako pierwszy
- Filtr kontrolera (jeśli istnieje) - sprawdzany jako drugi
- Filtr globalny (jeśli istnieje) - sprawdzany jako ostatni
Wyjątek obsługuje tylko jeden filtr. Jeśli filtr metody go przejmie, filtr kontrolera i globalny nie zostaną wywołane. Gdy jednak @Catch() filtra metody wskazuje inny typ wyjątku, sprawa idzie wyżej, do kontrolera. Na tym samym poziomie filtr łapiący wszystko deklaruj przed bardziej szczegółowym, żeby ten drugi mógł obsłużyć swój typ.
W następnej lekcji zobaczysz, w którym miejscu cyklu życia żądania pracują filtry, a cały moduł 6 poświęcimy obsłudze błędów.
Pamiętaj: wyjątek to oskarżenie, a filtr to trybunał, który zamienia je w czytelny wyrok dla klienta.
Kod do tej lekcji: src/exception-filters.ts
1// Filtry wyjątków - Sądy i Trybunały Imperium
2import {
3 ExceptionFilter, Catch, ArgumentsHost,
4 HttpException, HttpStatus, NotFoundException,
5 BadRequestException, ForbiddenException,
6} from '@nestjs/common';
7
8console.log("Filtry wyjątków - system sądów imperium!");
9
10// ===========================================
11// 1. Wbudowane wyjątki NestJS
12// ===========================================
13
14function demonstrateExceptions() {
15 // 404 Not Found
16 // throw new NotFoundException('Tributum non inventum!');
17
18 // 400 Bad Request
19 // throw new BadRequestException('Brak wymaganych danych!');
20
21 // 403 Forbidden
22 // throw new ForbiddenException('Brak uprawnień!');
23
24 // Dowolny kod HTTP
25 // throw new HttpException('Wiadomość', HttpStatus.I_AM_A_TEAPOT);
26
27 console.log("Wbudowane wyjątki: NotFoundException, BadRequestException...");
28 console.log("Każdy generuje odpowiedni kod HTTP automatycznie");
29}
30
31demonstrateExceptions();
32
33// ===========================================
34// 2. Własny filtr wyjątków
35// ===========================================
36
37@Catch(HttpException)
38class RomanExceptionFilter implements ExceptionFilter {
39 catch(exception: HttpException, host: ArgumentsHost) {
40 const ctx = host.switchToHttp();
41 const response = ctx.getResponse();
42 const request = ctx.getRequest();
43 const status = exception.getStatus();
44
45 response.status(status).json({
46 statusCode: status,
47 imperium: 'Roma Aeterna',
48 error: exception.message,
49 path: request.url,
50 timestamp: new Date().toISOString(),
51 });
52 }
53}
54
55console.log("\n@Catch(HttpException) - przechwytuje wyjątki HTTP");
56console.log("@Catch() - przechwytuje WSZYSTKIE wyjątki");
57
58// ===========================================
59// 3. Bindowanie filtrów
60// ===========================================
61
62console.log("\nPoziomy bindowania:");
63console.log("@UseFilters(filter) na metodzie - tylko ta metoda");
64console.log("@UseFilters(filter) na kontrolerze - cały kontroler");
65console.log("app.useGlobalFilters(filter) - cała aplikacja");
66
67console.log("\n=== PODSUMOWANIE FILTRÓW WYJĄTKÓW ===");
68console.log("HttpException - bazowy wyjątek HTTP");
69console.log("@Catch(Type) - dekorator filtra wyjątków");
70console.log("ExceptionFilter - interfejs do implementacji");
71console.log("@UseFilters() - bindowanie na metodę/kontroler");
72console.log("Kolejność: metoda > kontroler > globalny");
73Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki kod HTTP generuje rzucenie new NotFoundException() w NestJS?
2. Co oznacza dekorator @Catch(HttpException) na klasie filtru wyjątków?
3. Jak stworzyć filtr wyjątków przechwytujący WSZYSTKIE typy wyjątków (nie tylko HttpException)?
4. Jeśli zdefiniowano filtr na poziomie metody i kontrolera, który zostanie wykonany jako pierwszy?
Zadania praktyczne w grze
- Edytor kodu
Napisz RomanExceptionFilter z @Catch(HttpException), który zwraca JSON z polami: statusCode, imperium, error, path, timestamp
- Układanie w pionie
Uporządkuj poziomy bindowania Exception Filters od najwęższego do najszerszego zasięgu:
- Klikanie w kolejności
Ułóż elementy metody catch() filtra wyjątków w prawidłowej kolejności:
- Edytor kodu
Napisz AllExceptionsFilter z @Catch() (bez argumentów), który obsługuje zarówno HttpException, jak i inne błędy
- Układanie w poziomie
Ułóż elementy rzucenia wyjątku 404 w NestJS: