Ibiyemi Adewakun Ibiyemi is a full-stack developer from Lagos. When she's not writing code, she likes to read, listen to music, and put cute outfits together.

Exploring NestJS middleware: Benefits, use cases, and more

7 min read 2135 109

Exploring Nestjs Middleware Benefits, Use Cases, And More

Backend developers often apply some common tasks to the requests that our service receives.

Some of these tasks are applied before fulfilling the request, like authentication and authorization. Others are applied after the request is processed, but just before the response is sent, such as a log of the resource accessed.

Instead of adding this logic to our controllers, we can add these tasks to middleware and apply them to all routes or multiple specific routes.

In this article, we will look at understanding, creating, and applying middleware in NestJS. We will also discuss its benefits, along with common tasks delegated to Nest middleware.

Jump ahead:

To follow along with this tutorial, you’ll need to install Node.js v14 or newer, a JavaScript package manager, and an IDE or text editor of your choice. You’ll also need to create a NestJS project — or, you can follow along with the project referenced here.

What is middleware in NestJS?

Middleware is a function that is called before a route handler, also called a controller. It can also be configured to be called after the route handler executes the logic, but before the response is sent. These functions get access to the request and response objects and perform actions on them.

In NestJS, middleware can:

  • Perform any operations
  • Access and make changes to the request and response objects
  • Call the next middleware function in the stack
  • End the request-response cycle

Note that you must call the next() middleware function to pass control to the next middleware function if it does not end the request-response cycle. Otherwise, the request will be left hanging.

Creating NestJS middleware

NestJS middleware can be written as either a function or a class that uses the @Injectable() decorator and implements the NestMiddleware interface.

@Injectable() tells Nest to instantiate that class and inject the instance everywhere it is a dependency. This means if you define a class that needs an instance of your @Injectable() class, you will not have to create and assign the instance yourself.

Now that we understand how NestJS creates middleware, we will put it into practice.

Creating a class middleware

At this point, you should have created your NestJS project and added some routes and controllers, or simply forked the project we used here.

To keep things organized, we will create a directory middleware in our src directory:

$ cd src
$ mkdir middleware && cd middleware

Now that we have middleware, we will create our class file simple-logger.middleware.ts and fill it in like so:

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

@Injectable()
export class SimpleLoggerMiddleware implements NestMiddleware {
   use(req: Request, res: Response, next: NextFunction) {
      // do some tasks
      console.log('Executing request...');
      next();
   }
}

In the lines above, we created a simple middleware class called SimpleLoggerMiddleware.

The most important feature of our class is the use method, which is defined and required by the NestMiddleware. Any logic we would like our middleware to execute — before or after — in our request/response cycle must be written here.



Creating a function middleware

In our src/middleware directory, let’s create a new file simple-func.middleware.ts in which we will create and export a function that can be applied as a middleware:

import { Request, Response, NextFunction } from 'express'

export function simpleFunc(req: Request, res: Response, next: NextFunction) {
  // In here do some stuff :p
  console.log('Executing request after the function middleware...');
  next();
} 

You’ll notice that our function above is pretty similar to the use method of our SimpleLoggerMiddleware class; it takes Request, Response, and NextFunction instances as arguments.

Applying our NestJS middleware in a module class

Our middleware can be applied in our module class — not within the @Module() decorator where we typically register dependencies of a module, like imports, controllers, services, and others.

Here’s an example of dependencies registered in the @Module() decorator:

// tree.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TreeController } from './tree.controller';
import { Tree } from './tree.entity';
import { TreeService } from './tree.service';

@Module({
  imports: [TypeOrmModule.forFeature([Tree])],
  providers: [TreeService],
  controllers: [TreeController],
})

export class TreeModule {}

When it comes to middleware, we instead apply the consumer in the module class — in this example, the TreeModule class — using the configure() method made available by implementing the NestModule interface.

Here’s how it is implemented:

// tree.module.ts

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TreeController } from './tree.controller';
import { Tree } from './tree.entity';
import { TreeService } from './tree.service';
import { SimpleLoggerMiddleware } from '../middleware';

@Module({
  imports: [TypeOrmModule.forFeature([Tree])],
  providers: [TreeService],
  controllers: [TreeController],
})
export class TreeModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(SimpleLoggerMiddleware).forRoutes('trees');
  }
}

In our snippet above, in our module class TreeModule, the configure() method takes a middleware consumer. This consumer applies our middleware using the apply() method.

Then, we go a step further to tell the consumer to only apply the middleware to specific routes using the forRoutes() method. With this code, we have applied our SimpleLoggerMiddleware on all /trees route requests.

We can also make our applied middleware relevant to more specific routes. Using forRoutes, we can pass the resource and resource method that we want to be intercepted by the middleware.

Here is an example of using forRoutes to apply the middleware only to POST method requests on the /trees resource:

.forRoutes({ path: 'trees', method: RequestMethod.POST })

To make importing our middleware from the middleware directory easier to read, I have added a middleware/index.ts file and exported all the middleware we wrote in that directory:

export * from './simple-logger.middleware';
export * from './simple-func.middleware';

Applying middleware for all routes in a module class

In this section, let’s apply our function middleware simpleFunc to all routes and register it in the AppModule class:

// app.module.ts

import { TypeOrmModule } from '@nestjs/typeorm';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TreeModule } from './tree/tree.module';
import { Tree } from './tree/tree.entity';
import { simpleFunc } from './middleware';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'xxxx',
      database: 'fruit-tree',
      entities: [Tree],
      synchronize: true,
    }),
    TreeModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(simpleFunc).forRoutes('*');
  }
}

In our AppModule class, we implemented the NestModule along with the configure() method and applied our simpleFunc function middleware to all routes using .forRoutes('*') .


More great articles from LogRocket:


If we decide to exclude some specific routes — for example, a health check route like /healthcheck — we can chain our consumer.apply(…) with the method exclude() like so:

// app.module.ts

...

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(simpleFunc).exclude('healthcheck').forRoutes('*');
  }
}

The consumer methods apply(), exclude(), and forRoutes also accept a list of comma-separated values to allow us to register multiple middlewares and routes respectively.

Applying middleware globally in our application

We’ve looked at applying our middleware in module classes, but we can also apply a middleware — or several middleware — to all routes using the use() method of our Nest application’s INestApplication instance.

Our Nest application was instantiated in the main.ts file. Let’s use SimpleLoggerMiddleware there so we can add our global middleware:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { SimpleLoggerMiddleware } from './middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(SimpleLoggerMiddleware);
  await app.listen(3000);
}

bootstrap();

In the above snippet, we have registered the middleware on the Nest application instance, making it applicable to every route.

Common middleware used in NestJS projects

As we mentioned earlier, middleware are just functions that perform tasks before fulfilling a request-response cycle. There are a few middleware tasks that you will frequently find in NestJS projects.

Authentication and authorization

Although these terms are often used interchangeably, authentication and authorization mean and serve different purposes in our applications.

Authentication refers to checking the identity of a user or service — sometimes called clients — trying to access our application. We often confirm the validity and identify this identity using API keys or access tokens. With this identity information, we can then authorize access to a resource.

Authorization, on the other hand, refers to checking that the user or client identified during authentication is allowed to access a given resource. Authorization is usually done using the identity alongside permissions or scopes.

Both of these mechanisms are popular tasks used to protect resources, so they run before the request-response cycle is executed and are almost always applied as middleware to our resources.

You can write your own authentication and authorization middleware or use popular libraries like Auth0 or Passport. To use these libraries, simply register them like we did our example middleware.

CORS

CORS stands for Cross-Origin Resource Sharing. Simply put, it is a mechanism for determining that an application with a different origin can access resources of your application. This check is typically done before the request is processed, which makes it a pretty popular middleware.

An example of this could be configuring your app on fruit-trees.com to allow requests from your web application on cats-amazing.org.

We can apply CORS as middleware in NestJS when it’s configured to use the Express framework. To do this, we will first need to install the CORS Express middleware package — along with the necessary typings, since we’re working in TypeScript — using the following command:

$ yarn add cors
$ yarn add -D @types/cors

Then, wherever we would like to apply the CORS middleware — in this case, let’s use it our AppModule class — we will import and apply it to the middleware consumer:

// app.module.ts

import cors from 'cors';

...

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(simpleFunc, cors()).forRoutes('*');
  }
}

CORS does not necessarily have to be applied as a middleware, but since it can be, we can think of it as one here.

Helmet

Helmet is a Node.js library that helps set some important HTTP headers by default. These headers can help minimize some well-known security vulnerabilities. While Helmet doesn’t solve all application vulnerabilities, is a big help.

Helmet a pretty popular tool that can be set up as middleware in NestJS with Express. To apply it as middleware, we first need to install the helmet package like so:

$ yarn add helmet

Just like we did with CORS, we will import and apply the Helmet middleware to our middleware consumer. We’ll again add it to our AppModule class as an example:

// app.module.ts

import helmet from 'helmet';

...

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(simpleFunc, cors(), helmet()).forRoutes('*');
  }
}

Benefits of middleware in NestJS

Using middleware in your NestJS application is a good way to separate the core functionality of a resource from chores that need to be performed before or after the request is processed.

Middleware also provides a good way to reapply this chore logic where needed by being able to configure it for different resources — possibly in different modules — without repeating the logic.

Drawbacks of using middleware in NestJS

Using middleware in your applications is generally good practice, but some drawbacks can be experienced when not used correctly.

Middleware can slow down your application server

This can happen when middleware function executions are not terminated correctly using the next() function. By not calling the next() function, our requests can remain stuck in the middleware, causing the server to be overloaded and ultimately slower.

Middleware can be a source of bugs and unintended behaviors

When written correctly, middleware functions should be written with single jobs so it is clear what applying them to a resource or route does.

But if written poorly — for example, by combining multiple actions in one middleware function — we might alter the expected request or response for the application resulting in unintended behaviour.

Middleware can be a security risk

Since middleware have the ability to alter request and response objects, there are opportunities to alter these objects to expose or delete relevant data. This can pose an additional risk to users making requests to the application.

Conclusion

In this article, we took a look at middleware, its benefits and drawbacks, some of its common applications, and how it can be applied in NestJS applications. All the code referenced in this article is available on GitHub. Please share your comments and questions below!

Get setup with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Ibiyemi Adewakun Ibiyemi is a full-stack developer from Lagos. When she's not writing code, she likes to read, listen to music, and put cute outfits together.

Leave a Reply