Kurs NestJS · Moduł 2: Routing i cykl żądania
Middleware - pierwszy posterunek na drodze żądania
W tej lekcji6
Trasy z poprzedniej lekcji mówią, dokąd trafi żądanie. Ta lekcja jest o tym, co dzieje się po drodze - bo między wejściem żądania do aplikacji a uruchomieniem metody kontrolera NestJS ustawia cztery posterunki: middleware, guards, interceptors i pipes. Poznasz je po kolei; zaczynamy od pierwszego.
Middleware to rogatka przy trakcie. Stoi najdalej od miasta, przepuszcza wszystkich i każdego zapisuje w rejestrze - kupca, posłańca, legionistę. Nie pyta, dokąd kto zmierza, bo na rogatce jeszcze tego nie wiadomo.
Interfejs NestMiddleware
Middleware musi implementować interfejs NestMiddleware. Nie CanActivate - to interfejs guardów z następnej lekcji; nie NestInterceptor - to interceptory; nie ExceptionFilter - ten zajmuje się błędami. Interfejs wymaga jednej metody, use, o trzech argumentach:
1import { Injectable, NestMiddleware } from '@nestjs/common';
2import { Request, Response, NextFunction } from 'express';
3
4@Injectable()
5export class LoggerMiddleware implements NestMiddleware {
6 use(req: Request, res: Response, next: NextFunction) {
7 console.log(req.method, req.originalUrl);
8
9 next();
10 }
11}Trzy argumenty to trzy role. req (Request) niesie wszystko, co przyszło od klienta: metodę HTTP, adres, nagłówki, ciało żądania. res (Response) pozwala odpowiedzieć - ustawić status, nagłówek, wysłać treść. next (NextFunction) to przepustka: wywołanie next() przekazuje żądanie dalej.
Powyższy middleware zapisuje w konsoli metodę i adres, po czym puszcza żądanie dalej. Tyle wystarczy, żeby mieć rejestr wszystkiego, co wchodzi do aplikacji.
next() - przepustka, o której łatwo zapomnieć
Wywołanie next() nie jest formalnością. Jeśli middleware nie wywoła next(), żądanie zostanie zawieszone - nie przejdzie dalej w łańcuchu. Nie zobaczysz błędu ani wyjątku: serwer się nie zrestartuje, middleware nie wykona się drugi raz, a żądanie nie przejdzie samo do kontrolera. Klient po prostu czeka, aż upłynie jego limit czasu.
To najczęstsza pomyłka przy pisaniu middleware i jedyna, która nie zostawia żadnego śladu w logach.
Czasem jednak chcemy zatrzymać żądanie - i wtedy obowiązuje jedna zasada: skoro nie przekazujesz go dalej, musisz sam zamknąć odpowiedź:
1@Injectable()
2export class TollMiddleware implements NestMiddleware {
3 use(req: Request, res: Response, next: NextFunction) {
4 if (!req.headers['x-toll-paid']) {
5 res.status(402).json({ message: 'Myto nieopłacone' });
6
7 return;
8 }
9
10 next();
11 }
12}Każda ścieżka przez metodę use musi kończyć się albo wywołaniem next(), albo odpowiedzią wysłaną przez res. Trzeciej możliwości nie ma - a raczej jest, i nazywa się zawieszonym żądaniem.
Rejestracja - cztery kroki
Sama klasa nic nie robi, dopóki nie powiesz, gdzie ma działać. Proces ma cztery kroki, w tej kolejności:
- Utworzenie klasy middleware z
@Injectable(). - Implementacja interfejsu
NestModulew klasie modułu. - Wywołanie
consumer.apply(Middleware)w metodzieconfigure(). - Określenie tras za pomocą
.forRoutes().
Krok pierwszy masz już za sobą - to klasa z poprzedniego przykładu. Pozostałe trzy dzieją się w module:
1import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
2
3@Module({
4 controllers: [TributesController],
5 providers: [TributesService],
6})
7export class TributesModule implements NestModule {
8 configure(consumer: MiddlewareConsumer) {
9 consumer.apply(LoggerMiddleware).forRoutes('tributes');
10 }
11}implements NestModule to obietnica, że klasa dostarczy metodę configure - NestJS wywoła ją przy budowaniu modułu. Wewnątrz consumer typu MiddlewareConsumer przyjmuje dwie informacje: .apply(LoggerMiddleware) mówi co podpiąć, .forRoutes('tributes') mówi gdzie.
Dwie rzeczy łatwo przeoczyć. Samo implements NestModule niczego nie uruchamia - to kontrakt dla TypeScriptu, który pilnuje nazwy metody i typu argumentu; NestJS wywołuje configure() w każdym module, który ją ma. Za to consumer.apply(LoggerMiddleware) bez .forRoutes() nie podpina middleware pod żadną trasę - i znowu bez żadnego błędu.
Zasięg - od jednej trasy do całej aplikacji
.forRoutes() przyjmuje kilka postaci. Napis 'tributes' obejmuje wszystkie trasy zaczynające się od /tributes. Klasa kontrolera, na przykład TributesController, obejmuje wszystkie jego trasy. Obiekt { path: 'tributes', method: RequestMethod.POST } zawęża rzecz do jednej metody HTTP. Można też wykluczać trasy przez .exclude() - przydaje się przy /health, którego nie ma sensu logować.
Gdy middleware ma obejmować całą aplikację, nie potrzebujesz modułu:
1async function bootstrap() {
2 const app = await NestFactory.create(AppModule);
3
4 app.use(helmet());
5
6 await app.listen(3000);
7}app.use() w main.ts podpina middleware globalnie i przyjmuje także zwykłe funkcje Express - dlatego tą drogą włącza się gotowe biblioteki, jak helmet czy cors. Różnica jest jedna: middleware zarejestrowane przez app.use() nie przechodzi przez wstrzykiwanie zależności, więc nie ma dostępu do serwisów aplikacji.
Gdzie kończy się middleware
Rejestr na rogatce, nagłówki CORS, parsowanie ciasteczek - to wszystko rzeczy niezależne od tego, dokąd żądanie zmierza. I dobrze, bo middleware działa na poziomie Express, zanim NestJS rozstrzygnie, który kontroler i która metoda obsłużą żądanie. Dostaje req, res i next - trzy obiekty HTTP i nic ponadto.
Decyzje w rodzaju „ten endpoint wymaga roli senatora" wymagają wiedzy, której na rogatce nie ma. Do tego służy drugi posterunek, i o nim będzie następna lekcja.
Podsumowanie
Rogatka zapisuje każdego i puszcza dalej:
- na drodze żądania stoją cztery mechanizmy: middleware → guards → interceptors → pipes; middleware jest pierwszy,
- middleware implementuje interfejs
NestMiddleware- nieCanActivate, nieNestInterceptor, nieExceptionFilter, - metoda
use(req: Request, res: Response, next: NextFunction):reqniesie żądanie,respozwala odpowiedzieć,nextprzekazuje dalej, - bez wywołania
next()żądanie zostaje zawieszone i nie przejdzie dalej w łańcuchu - serwer się nie zrestartuje, middleware nie wykona się dwa razy, żądanie nie trafi samo do kontrolera, - każda ścieżka przez
usekończy się albonext(), albo odpowiedzią wysłaną przezres, - rejestracja w czterech krokach: klasa z
@Injectable()→implements NestModulew module →consumer.apply(Middleware)wconfigure()→.forRoutes(), .forRoutes()przyjmuje ścieżkę, klasę kontrolera albo obiekt zpathimethod;.exclude()wyłącza wybrane trasy,app.use()wmain.tspodpina middleware globalnie, ale bez wstrzykiwania zależności,- middleware działa przed rozstrzygnięciem trasy docelowej, więc nadaje się do logowania i nagłówków, a nie do decyzji zależnych od endpointu.
W następnej lekcji poznasz guards - drugi posterunek, który jako pierwszy wie, dokąd żądanie zmierza, i dlatego może powiedzieć „nie". A na razie zapamiętaj jedno: middleware, które nie wywoła next(), nie zgłasza błędu. Ono po prostu milknie.
Kod do tej lekcji: src/middleware.ts
1// Middleware w NestJS - Strażnicy Bram Forum
2import { Injectable, NestMiddleware } from '@nestjs/common';
3import { Request, Response, NextFunction } from 'express';
4
5console.log("Middleware - strażnicy kontrolujący ruch w imperium!");
6
7// ===========================================
8// 1. Klasa middleware - Logger
9// ===========================================
10
11@Injectable()
12export class RomanLoggerMiddleware implements NestMiddleware {
13 use(req: Request, res: Response, next: NextFunction) {
14 const startTime = Date.now();
15
16 console.log('[Roman Gate] ' + req.method + ' ' + req.originalUrl);
17 console.log('[Roman Gate] IP: ' + req.ip);
18
19 // Po zakończeniu odpowiedzi - logujemy czas
20 res.on('finish', () => {
21 const duration = Date.now() - startTime;
22 console.log('[Roman Gate] Status: ' + res.statusCode + ' (' + duration + 'ms)');
23 });
24
25 next(); // Przekaż sterowanie dalej
26 }
27}
28
29// ===========================================
30// 2. Middleware funkcyjne
31// ===========================================
32
33export function corsMiddleware(req: Request, res: Response, next: NextFunction) {
34 res.header('Access-Control-Allow-Origin', '*');
35 res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
36 res.header('X-Powered-By', 'Imperium Romanum');
37 console.log('[CORS] Nagłówki ustawione dla: ' + req.originalUrl);
38 next();
39}
40
41// ===========================================
42// 3. Rejestracja middleware w module
43// ===========================================
44
45import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
46
47@Module({
48 imports: [],
49 controllers: [],
50 providers: [],
51})
52export class AppModule implements NestModule {
53 configure(consumer: MiddlewareConsumer) {
54 // Middleware dla wszystkich tras
55 consumer
56 .apply(RomanLoggerMiddleware)
57 .forRoutes('*');
58
59 // Middleware tylko dla konkretnych tras
60 consumer
61 .apply(corsMiddleware)
62 .forRoutes(
63 { path: 'legiones', method: RequestMethod.ALL },
64 { path: 'tributes', method: RequestMethod.GET },
65 );
66 }
67}
68
69// ===========================================
70// 4. Middleware z wykluczeniem tras
71// ===========================================
72
73// consumer
74// .apply(AuthMiddleware)
75// .exclude(
76// { path: 'auth/login', method: RequestMethod.POST },
77// { path: 'auth/register', method: RequestMethod.POST },
78// )
79// .forRoutes('*');
80
81console.log("\n=== PODSUMOWANIE MIDDLEWARE ===");
82console.log("Middleware działa między żądaniem a odpowiedzią");
83console.log("@Injectable() + NestMiddleware - klasa middleware");
84console.log("Funkcja (req, res, next) - funkcyjne middleware");
85console.log("consumer.apply().forRoutes() - rejestracja w module");
86console.log("next() - przekazuje sterowanie dalej");
87Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaki interfejs musi implementować middleware w NestJS?
2. Co się stanie, jeśli middleware NIE wywoła funkcji next()?
Zadania praktyczne w grze
- Edytor kodu
Napisz LoggerMiddleware implementujący NestMiddleware z metodą use(req, res, next), która loguje metodę i URL żądania, a potem wywołuje next()
- Układanie w pionie
Uporządkuj kroki rejestracji middleware w NestJS od pierwszego do ostatniego: