NestJS course Β· Module 2: Routing and Request Lifecycle
Middleware - the first post on a request's road
In this lesson6
The routes of the previous lesson say where a request goes. This lesson is about what happens on the way - because between a request entering the application and a controller method running, NestJS sets four posts: middleware, guards, interceptors and pipes. You will meet them in turn; we begin with the first.
Middleware is the toll post on the road. It stands furthest from the city, lets everyone through and writes each one into the register - merchant, courier, legionary. It does not ask where anyone is headed, because at the toll post that is not yet known.
The NestMiddleware interface
Middleware must implement the NestMiddleware interface. Not CanActivate - that is the guards' interface from the next lesson; not NestInterceptor - those are interceptors; not ExceptionFilter - that one deals with errors. The interface calls for a single method, use, taking three arguments:
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}Three arguments, three roles. req (Request) carries everything that came from the client: the HTTP method, the address, the headers, the request body. res (Response) lets you answer - set a status, a header, send content. next (NextFunction) is the pass: calling next() hands the request onward.
The middleware above writes the method and address to the console, then lets the request through. That much is enough to keep a register of everything entering the application.
next() - the pass that is easy to forget
Calling next() is no formality. If middleware does not call next(), the request will hang - it will not proceed further in the chain. You will see no error and no exception: the server does not restart, the middleware does not execute twice, and the request does not pass to the controller on its own. The client simply waits until its timeout runs out.
This is the commonest mistake in writing middleware, and the only one that leaves no trace in the logs.
Sometimes, though, we want to stop a request - and then one rule applies: since you are not passing it on, you must close the response yourself:
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: 'Toll unpaid' });
6
7 return;
8 }
9
10 next();
11 }
12}Every path through the use method must end either in a call to next() or in a response sent through res. There is no third possibility - or rather there is, and it is called a hung request.
Registration - four steps
The class alone does nothing until you say where it should run. The process has four steps, in this order:
- Creating a middleware class with
@Injectable(). - Implementing the
NestModuleinterface in the module class. - Calling
consumer.apply(Middleware)in theconfigure()method. - Specifying routes using
.forRoutes().
The first step is already behind you - it is the class from the previous example. The other three happen in a 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 is a promise that the class will supply a configure method - NestJS calls it while building the module. Inside, the consumer of type MiddlewareConsumer takes two pieces of information: .apply(LoggerMiddleware) says what to attach, .forRoutes('tributes') says where.
Two things are easy to miss. implements NestModule on its own runs nothing - it is a contract for TypeScript that checks the method name and the argument type; NestJS calls configure() in every module that has it. But consumer.apply(LoggerMiddleware) without .forRoutes() attaches the middleware to no route at all - again with no error.
Scope - from one route to the whole application
.forRoutes() takes several forms. The string 'tributes' covers every route beginning with /tributes. A controller class, say TributesController, covers all of its routes. The object { path: 'tributes', method: RequestMethod.POST } narrows things to a single HTTP method. Routes can also be excluded with .exclude() - useful for /health, which there is no sense in logging.
When middleware is to cover the whole application, you need no module:
1async function bootstrap() {
2 const app = await NestFactory.create(AppModule);
3
4 app.use(helmet());
5
6 await app.listen(3000);
7}app.use() in main.ts attaches middleware globally and accepts plain Express functions too - which is why ready-made libraries such as helmet and cors are switched on this way. There is one difference: middleware registered through app.use() does not go through dependency injection, so it has no access to the application's services.
Where middleware ends
The register at the toll post, CORS headers, cookie parsing - all of these are independent of where a request is headed. And rightly so, because middleware runs at the Express level, before NestJS has settled which controller and which method will handle the request. It receives req, res and next - three HTTP objects and nothing beyond them.
Decisions of the kind "this endpoint requires a senator's rank" need knowledge the toll post does not have. That is what the second post is for, and it is the subject of the next lesson.
Summary
The toll post registers everyone and lets them through:
- four mechanisms stand on a request's road: middleware β guards β interceptors β pipes; middleware is the first,
- middleware implements the
NestMiddlewareinterface - notCanActivate, notNestInterceptor, notExceptionFilter, - the method
use(req: Request, res: Response, next: NextFunction):reqcarries the request,reslets you answer,nexthands it onward, - without a call to
next()the request hangs and will not proceed further in the chain - the server does not restart, the middleware does not run twice, the request does not reach the controller by itself, - every path through
useends either innext()or in a response sent throughres, - registration in four steps: a class with
@Injectable()βimplements NestModulein the module βconsumer.apply(Middleware)inconfigure()β.forRoutes(), .forRoutes()takes a path, a controller class, or an object withpathandmethod;.exclude()leaves chosen routes out,app.use()inmain.tsattaches middleware globally, but without dependency injection,- middleware runs before the target route is settled, so it suits logging and headers rather than decisions that depend on the endpoint.
In the next lesson you will meet guards - the second post, the first to know where a request is headed and therefore able to say "no". For now remember one thing: middleware that fails to call next() reports no error. It simply falls silent.
Code for this lesson: src/middleware.ts
1// Middleware in NestJS - Forum Gate Guards
2import { Injectable, NestMiddleware } from '@nestjs/common';
3import { Request, Response, NextFunction } from 'express';
4
5console.log("Middleware - guards controlling traffic in the empire!");
6
7// ===========================================
8// 1. Middleware class - 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 // After response finishes - log the time
20 res.on('finish', () => {
21 const duration = Date.now() - startTime;
22 console.log('[Roman Gate] Status: ' + res.statusCode + ' (' + duration + 'ms)');
23 });
24
25 next(); // Pass control forward
26 }
27}
28
29// ===========================================
30// 2. Functional middleware
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] Headers set for: ' + req.originalUrl);
38 next();
39}
40
41// ===========================================
42// 3. Middleware registration in 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 for all routes
55 consumer
56 .apply(RomanLoggerMiddleware)
57 .forRoutes('*');
58
59 // Middleware only for specific routes
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 with route exclusion
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=== MIDDLEWARE SUMMARY ===");
82console.log("Middleware works between request and response");
83console.log("@Injectable() + NestMiddleware - middleware class");
84console.log("Function (req, res, next) - functional middleware");
85console.log("consumer.apply().forRoutes() - registration in module");
86console.log("next() - passes control forward");
87Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What interface must a middleware implement in NestJS?
2. What happens if middleware does NOT call the next() function?
Hands-on tasks in the game
- Code editor
Write a LoggerMiddleware implementing NestMiddleware with a use(req, res, next) method that logs the request method and URL and then calls next()
- Vertical ordering
Order the steps for registering middleware in NestJS from first to last: