DEV Community

Cover image for Middleware and Interceptors in NestJS: Best Practices
Ezile Mdodana
Ezile Mdodana

Posted on

Middleware and Interceptors in NestJS: Best Practices

NestJS is a progressive Node.js framework for building efficient and scalable server-side applications. Among its many powerful features, middleware and interceptors stand out as essential tools for handling cross-cutting concerns in a clean and reusable manner. In this article, we'll explore the concepts of middleware and interceptors, their use cases, and best practices for implementing them in your NestJS applications.

Middleware in NestJS

What is Middleware?

Middleware functions are functions that have access to the request and response objects, and the next middleware function in the application's request-response cycle. Middleware can perform a variety of tasks, such as logging, authentication, parsing, and more.

Creating Middleware

In NestJS, middleware can be created as either a function or a class implementing the NestMiddleware interface. Here’s an example of both approaches:

Function-based Middleware

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

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`Request...`);
  next();
}
Enter fullscreen mode Exit fullscreen mode

Class-based Middleware

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

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

Applying Middleware

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(logger);
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

To apply middleware to specific routes, use the configure method in a module:

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class CatsModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes(CatsController);
  }
}
Enter fullscreen mode Exit fullscreen mode

Use Cases for Middleware

  • Logging: Log requests for debugging and analytics.
  • Authentication: Check if a user is authenticated before proceeding.
  • Request Parsing: Parse incoming request bodies (e.g., JSON, URL-encoded).

Interceptors in NestJS

What is an Interceptor?

Interceptors are used to perform actions before and after the execution of route handlers. They can transform request/response data, handle logging, or modify the function execution flow.

Creating Interceptors

Interceptors are implemented using the NestInterceptor interface and the @Injectable decorator. Here’s an example of a basic interceptor:

import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, any> {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<any> {
    return next
      .handle()
      .pipe(map(data => ({ data })));
  }
}
Enter fullscreen mode Exit fullscreen mode

Applying Interceptors

Interceptors can be applied globally, at the controller level, or at the route handler level.

Global Interceptors

To apply an interceptor globally, use the useGlobalInterceptors method in the main.ts file:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalInterceptors(new TransformInterceptor());
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Controller-level Interceptors

To apply an interceptor at the controller level, use the @UseInterceptors decorator:

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';

@Controller('cats')
@UseInterceptors(TransformInterceptor)
export class CatsController {
  @Get()
  findAll() {
    return { message: 'This action returns all cats' };
  }
}
Enter fullscreen mode Exit fullscreen mode

Route-level Interceptors

To apply an interceptor at the route handler level, use the @UseInterceptors decorator directly on the method:

import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';

@Controller('cats')
export class CatsController {
  @Get()
  @UseInterceptors(TransformInterceptor)
  findAll() {
    return { message: 'This action returns all cats' };
  }
}
Enter fullscreen mode Exit fullscreen mode

Use Cases for Interceptors

  • Response Transformation: Modify response data before sending it to the client.
  • Logging: Log method execution time and other details.
  • Exception Mapping: Transform exceptions into user-friendly error messages.
  • Caching: Implement caching mechanisms for repeated requests.

Best Practices

  • Keep Middleware Lightweight: Middleware should be focused on tasks that need to be performed on every request, such as logging or authentication. Avoid heavy processing in middleware.

  • Use Interceptors for Transformation: Use interceptors for transforming request and response data, logging execution time, and handling errors.

  • Modularize Middleware and Interceptors: Create separate files and directories for middleware and interceptors to keep the codebase organized.

  • Leverage Dependency Injection: Use NestJS's dependency injection system to inject services into middleware and interceptors.

  • Avoid Redundant Code: Use global middleware and interceptors for tasks that need to be applied across the entire application, and use route-specific ones for more granular control.

Conclusion

Middleware and interceptors are powerful tools in NestJS that help you handle cross-cutting concerns effectively. By following best practices and understanding their use cases, you can create more maintainable and scalable NestJS applications. Whether you're logging requests, handling authentication, transforming responses, or caching data, middleware and interceptors provide the flexibility and control needed to build robust applications.

My way is not the only way!

Top comments (0)