Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Middleware - pierwszy posterunek na drodze żądania

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.

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.

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, @name:

  • 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.

Przejdź do CodeWorlds