InteractiveFrameworks

Middleware

Write a middleware class that records every request to the cats routes and nothing else, and apply it from the module with configure().

What you'll learn

  • Write middleware as an @Injectable() class implementing NestMiddleware, with a service injected
  • Apply middleware from a module's configure() with MiddlewareConsumer, to a controller's routes only
  • Explain why middleware must call next() or end the response, and what it cannot know about the handler

Some work belongs to every request and to no handler in particular: logging it, giving it an id, reading a session cookie, checking a header. Putting that in each handler would repeat it everywhere; putting it in a service still needs each handler to call it. Middleware is code that runs before the router chooses a handler, so it sees every request that matches a path, whatever the handler turns out to be.

Nest middleware is Express middleware. Anything written for Express (cors, helmet, cookie-parser, compression) can be applied as it is, which is a large part of why Nest sits on Express.

The one rule

A middleware function receives the request, the response and a next() function. It may run code, change the request, set headers on the response, and then it must do one of two things: call next() to hand over to whatever comes next, or end the response itself. If it does neither, the request hangs. Nothing else runs, and the client waits forever. Every middleware bug you will ever write is a missing next().

A middleware class

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  private counter = 0;

  use(req: Request, res: Response, next: NextFunction) {
    const id = String(++this.counter);
    res.setHeader('x-request-id', id);
    next();
  }
}

NestMiddleware asks for one method, use. req is Express's request: req.method, req.originalUrl (the full path as requested), req.headers, req.body. res is Express's response. The class is @Injectable(), and that is the reason to write middleware as a class rather than a function: it takes part in dependency injection like a provider and can ask for a service in its constructor, the way the middleware in this lesson asks for the log. Middleware that needs nothing can be a plain function with the same three parameters, which Nest calls functional middleware.

Applying it

Middleware is not listed in a module's metadata. A module applies it by implementing NestModule, whose one method, configure(), receives a MiddlewareConsumer:

@Module({ imports: [OrdersModule] })
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(RequestIdMiddleware).forRoutes('{*splat}');
  }
}

forRoutes() names what to cover, and takes three kinds of argument:

  • a path string: 'orders' is exactly /orders, 'orders/{*splat}' is everything under it, and '{*splat}' is every route;
  • a RouteInfo with a method: { path: 'orders', method: RequestMethod.POST };
  • a controller class, which covers every route the controller declares, parameters included.

Several middleware in one apply(a, b, c) run in that order. exclude() removes routes from a selection. Middleware that needs no dependencies can also be registered for everything with app.use(fn) in main.ts, but middleware registered that way cannot inject anything.

Where it sits

Middleware runs first, before guards, interceptors, pipes and the handler, and it does not know which handler will run: it sees a path and a method, nothing more. That is exactly why the tools of the next lessons exist. A guard knows the handler and its metadata; middleware does not. Use middleware for what Express middleware is for: logging, parsing, headers, sessions, and attaching things to the request for later stages to read.

An exception thrown in middleware goes to the exceptions layer like any other, but only a global exception filter sees it, because no controller has been chosen yet.

Your task

The starter has a RequestLog service that keeps lines, and a StatsController that returns them at GET /stats. Make RequestLogMiddleware record every request to the cats routes, and only those:

  1. In use(), record one line of the form METHOD /path for the request, print the same line to the console, and hand over.
  2. Make AppModule apply the middleware to every route of CatsController, and to nothing else.

The tests call three cats routes, then read /stats twice. The second read must show the same three lines, because /stats is not a cats route. Watch the console for your lines, between Nest's.

When it fails

  • A request never answers, and the grader reports a timeout: use() does not call next().
  • /stats returns 0 entries: the middleware is not applied. Check that AppModule implements NestModule and that configure() names the controller.
  • /stats counts 4 on the second read: the middleware covers /stats too. forRoutes(CatsController) covers only the controller's routes.
  • The line reads GET /: req.url is relative to the router's mount point. req.originalUrl is the path the client sent.

Remember

  • Middleware runs before routing, for every request that matches a path. It does not know the handler.
  • It must call next() or end the response.
  • A middleware class is @Injectable() and can receive services. Apply it in a module's configure().
  • forRoutes() takes a path, a RouteInfo or a controller class.
Stuck? Show a hint

The line is the request's method and its originalUrl. The log service has a record() method, and the middleware already receives it in its constructor. After recording and printing, call next(). In app.module.ts, implement NestModule: configure(consumer) applies RequestLogMiddleware and forRoutes() takes the controller class.