Kurs NestJS · Moduł 2: Routing i cykl żądania

Middleware - pierwszy posterunek na drodze żądania

5 min czytania
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:

  1. Utworzenie klasy middleware z @Injectable().
  2. Implementacja interfejsu NestModule w klasie modułu.
  3. Wywołanie consumer.apply(Middleware) w metodzie configure().
  4. 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 - nie CanActivate, nie NestInterceptor, nie ExceptionFilter,
  • metoda use(req: Request, res: Response, next: NextFunction): req niesie żądanie, res pozwala odpowiedzieć, next przekazuje 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 use kończy się albo next(), albo odpowiedzią wysłaną przez res,
  • rejestracja w czterech krokach: klasa z @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,
  • 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");
87

Widzisz błąd w tej lekcji?

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaki interfejs musi implementować middleware w NestJS?

  2. 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:

Przydatne artykuły