Middleware and Interceptors in NestJS: Best Practices
Tags: nestjs, backend, typescript, middleware
Published: 2024-06-14 10:04:12

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();
}
```

**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();
  }
}
```

**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();
```

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);
  }
}
```

**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 })));
  }
}
```

**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();
```


**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' };
  }
}
```


**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' };
  }
}
```

**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!**