We use cookies to enhance your experience on the site
CodeWorlds

Middleware - the first post on a request's road

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:

  1. Creating a middleware class with
    @Injectable()
    .
  2. Implementing the
    NestModule
    interface in the module class.
  3. Calling
    consumer.apply(Middleware)
    in the
    configure()
    method.
  4. 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.

The order of the steps matters in practice: a forgotten

implements NestModule
leaves the
configure
method sitting there with nobody to call it, and the middleware never runs - again with no error at all.

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

  • four mechanisms stand on a request's road: middleware → guards → interceptors → pipes; middleware is the first,
  • middleware implements the
    NestMiddleware
    interface
    - not
    CanActivate
    , not
    NestInterceptor
    , not
    ExceptionFilter
    ,
  • the method
    use(req: Request, res: Response, next: NextFunction)
    :
    req
    carries the request,
    res
    lets you answer,
    next
    hands 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
    use
    ends either in
    next()
    or in a response sent through
    res
    ,
  • registration in four steps: a class with
    @Injectable()
    implements NestModule
    in the module →
    consumer.apply(Middleware)
    in
    configure()
    .forRoutes()
    ,
  • .forRoutes()
    takes a path, a controller class, or an object with
    path
    and
    method
    ;
    .exclude()
    leaves chosen routes out,
  • app.use()
    in
    main.ts
    attaches 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.

Go to CodeWorlds