Middleware is a fundamental aspect of any modern web application. In NestJS, a versatile, extensible framework for building efficient, scalable Node.js server-side applications, middleware plays a crucial role. Extending the capabilities of traditional middleware systems, NestJS empowers developers to create complex, feature-rich backend systems. This guide will elucidate the concepts, creation, and mastery of middleware in NestJS.

What is Middleware in NestJS?

Middleware, in a nutshell, are functions running before the route handlers in any web application. They operate on the request and response objects, performing tasks such as authentication, logging, or data parsing and have the power to decide whether to pass the request to the next middleware or to end the HTTP request-response cycle right there.

A NestJS middleware can be a function or a class decorated with @Injectable(). When defined as a class, the class should implement the NestMiddleware interface.

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

@Injectable()
export class LogMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...', req);
    next();
  }
}

In this example, LogMiddleware logs each incoming request and then passes it on to the next middleware/ route handler via the next() function.

Applying Middleware

Middleware is usually implemented in the configure() method of a module that implements NestModule interface.

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LogMiddleware } from './middlewares/log.middleware';

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LogMiddleware)
      .forRoutes('*');
  }
}

In this code snippet, LogMiddleware will apply to all routes. Contrarily, you can list specific routes, allowing you to customize exactly when your middleware is invoked.

Refining Middleware Application

Middleware can be applied globally, used specifically for certain HTTP methods and even be excluded from functioning on certain routes.

Applying Middleware Globally

To exert middleware on every incoming request regardless of the route, apply it globally as seen below:

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

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

Here, LogMiddleware is applied to all incoming requests.

Specifying Middleware for Particular HTTP Methods

You can influence middleware to function on certain HTTP methods (GET, POST, PUT, DELETE etc.) using the RequestMethod enum.

import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LogMiddleware } from './middlewares/log.middleware';

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LogMiddleware)
      .forRoutes({ path: 'users', method: RequestMethod.GET });
  }
}

In this case, LogMiddleware applies only to GET requests made on the 'users' route.

Excluding Middleware from Certain Routes

Alternatively, you can indicate routes that will not use certain middleware using the exclude() method.

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LogMiddleware } from './middlewares/log.middleware';

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LogMiddleware)
      .exclude('login', 'signup')
      .forRoutes('*');
  }
}

In this code, all routes except login and signup will apply LogMiddleware.

Real-World Use Cases of Middleware

Middleware boasts a wide spectrum of applications, some of which are described below.

Logging

Middleware is ideal for creating logs about incoming requests. By logging details about the request, you can keep track of all network traffic, enabling you to spot patterns or potential problem areas.

Authentication

Middleware can control access to routes depending on the user's authentication status. You can design your middleware to decode and validate tokens attached to requests, adding the decoded user information to the request. This authenticated user then can be used directly by route handlers without having to validate the user there.

API Key Validation

Middleware can vet incoming requests for valid API keys. By checking the API key in the header of incoming requests, the middleware can filter out unauthorized requests even before they reach the route handler.

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

@Injectable()
export class ApiKeyMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const apiKey = req.headers['x-api-key'];
    const validApiKey = '123456'; //This should not be in plain text!

    if (apiKey !== validApiKey) {
      throw new Error('Invalid API Key');
    }

    next();
  }
}


Conclusion

Middleware is a powerful, flexible feature in NestJS that allows developers to extend the functionality and control the behavior of their applications. By harnessing middleware to manage tasks such as logging, authentication, and API key validation, developers can create clean, maintainable, and efficient web applications with rich, robust backend systems.