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.
Middleware musi implementować interfejs
. Nie NestMiddleware
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.
(Request) niesie wszystko, co przyszło od klienta: metodę HTTP, adres, nagłówki, ciało żądania. req
(Response) pozwala odpowiedzieć - ustawić status, nagłówek, wysłać treść. res
(NextFunction) to przepustka: wywołanie next
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.
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.Sama klasa nic nie robi, dopóki nie powiesz, gdzie ma działać. Proces ma cztery kroki, w tej kolejności:
@Injectable().NestModule w klasie modułu.consumer.apply(Middleware) w metodzie configure()..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.Kolejność kroków ma znaczenie praktyczne: zapomniany
implements NestModule sprawia, że metoda configure istnieje, ale nikt jej nie wywoła, a middleware nigdy nie zadziała - i znowu bez żadnego błędu..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.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.
Rogatka zapisuje każdego i puszcza dalej, @name:
NestMiddleware - nie CanActivate, nie NestInterceptor, nie ExceptionFilter,use(req: Request, res: Response, next: NextFunction): req niesie żądanie, res pozwala odpowiedzieć, next przekazuje dalej,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,use kończy się albo next(), albo odpowiedzią wysłaną przez res,@Injectable() → implements NestModule w module → consumer.apply(Middleware) w configure() → .forRoutes(),.forRoutes() przyjmuje ścieżkę, klasę kontrolera albo obiekt z path i method; .exclude() wyłącza wybrane trasy,app.use() w main.ts podpina middleware globalnie, ale bez wstrzykiwania zależności,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.