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
RouteInfowith 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:
- In
use(), record one line of the formMETHOD /pathfor the request, print the same line to the console, and hand over. - Make
AppModuleapply the middleware to every route ofCatsController, 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 callnext(). /statsreturns 0 entries: the middleware is not applied. Check thatAppModuleimplementsNestModuleand thatconfigure()names the controller./statscounts 4 on the second read: the middleware covers/statstoo.forRoutes(CatsController)covers only the controller's routes.- The line reads
GET /:req.urlis relative to the router's mount point.req.originalUrlis 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'sconfigure(). forRoutes()takes a path, aRouteInfoor 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.