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.
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:
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.
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.
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.
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.
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';
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('*')
.
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.
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.
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.
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 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 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('*'); } }
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.
Using middleware in your applications is generally good practice, but some drawbacks can be experienced when not used correctly.
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.
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.
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.
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!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ 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>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
One Reply to "Exploring NestJS middleware: Benefits, use cases, and more"
It is really helpfull