Skip to content

Middleware, Guards & Interceptors

NestJS provides several mechanisms for handling cross-cutting concerns in your application. This chapter covers middleware, guards, interceptors, and exception filters.

Request Lifecycle

Understanding the order of execution is crucial. Every request goes through multiple layers:

┌─────────────────────────────────────────────────────────────────┐
│                    REQUEST LIFECYCLE                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  📥 Incoming Request                                             │
│         │                                                        │
│         ▼                                                        │
│  ┌─────────────────┐                                            │
│  │   Middleware    │ ← Logging, CORS, Body parsing              │
│  └────────┬────────┘                                            │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │     Guards      │ ← Authentication, Authorization            │
│  └────────┬────────┘   (Can block request here!)                │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │ Interceptors ⬇️  │ ← Transform request, Start timing          │
│  └────────┬────────┘                                            │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │     Pipes       │ ← Validation, Transformation               │
│  └────────┬────────┘                                            │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │   Controller    │ ← Your route handler                       │
│  │     ↓           │                                            │
│  │   Service       │ ← Business logic                           │
│  └────────┬────────┘                                            │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │ Interceptors ⬆️  │ ← Transform response, End timing          │
│  └────────┬────────┘                                            │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │Exception Filters│ ← Handle errors (if any thrown)            │
│  └────────┬────────┘                                            │
│           ▼                                                      │
│  📤 Response                                                     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Quick Reference:

LayerPurposeCan Stop Request?
MiddlewareLogging, CORS, body parsingYes (res.end())
GuardsAuth checkYes (return false)
Interceptors (pre)Transform requestYes (throw error)
PipesValidate/transform dataYes (validation error)
ControllerHandle request-
Interceptors (post)Transform response-
Exception FiltersHandle errorsCatches thrown errors

Middleware

Middleware functions have access to the request and response objects, and the next() function. They can execute code, modify request/response, end the request-response cycle, or call the next middleware.

Creating Middleware

typescript
// src/common/middleware/logger.middleware.ts
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) {
        const start = Date.now();

        console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);

        res.on('finish', () => {
            const duration = Date.now() - start;
            console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} - ${res.statusCode} (${duration}ms)`);
        });

        next();
    }
}

Applying Middleware

Middleware is applied in the module's configure method:

typescript
// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { UsersModule } from './users/users.module';

@Module({
    imports: [UsersModule],
})
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(LoggerMiddleware)
            .forRoutes('*'); // Apply to all routes
    }
}

Middleware Configuration Options

typescript
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(LoggerMiddleware)
            // Apply to specific routes
            .forRoutes('users')

            // Apply to specific methods
            .forRoutes({ path: 'users', method: RequestMethod.GET })

            // Apply to multiple routes
            .forRoutes(
                { path: 'users', method: RequestMethod.GET },
                { path: 'products', method: RequestMethod.ALL },
            )

            // Apply to a controller
            .forRoutes(UsersController)

            // Exclude specific routes
            .exclude(
                { path: 'users/login', method: RequestMethod.POST },
                { path: 'users/register', method: RequestMethod.POST },
            )
            .forRoutes(UsersController);
    }
}

Multiple Middleware

typescript
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(CorsMiddleware, LoggerMiddleware, AuthMiddleware)
            .forRoutes('*');
    }
}

Functional Middleware

For simple middleware, you can use a function:

typescript
// src/common/middleware/simple-logger.middleware.ts
import { Request, Response, NextFunction } from 'express';

export function simpleLogger(req: Request, res: Response, next: NextFunction) {
    console.log(`Request: ${req.method} ${req.url}`);
    next();
}

// Apply in module
consumer.apply(simpleLogger).forRoutes('*');

Global Middleware

Apply middleware globally in main.ts:

typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as helmet from 'helmet';
import * as compression from 'compression';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);

    // Global middleware
    app.use(helmet());
    app.use(compression());

    await app.listen(3000);
}
bootstrap();

Common Middleware Examples

CORS Middleware

typescript
@Injectable()
export class CorsMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction) {
        res.header('Access-Control-Allow-Origin', '*');
        res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH');
        res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');

        if (req.method === 'OPTIONS') {
            return res.sendStatus(200);
        }
        next();
    }
}

Request ID Middleware

typescript
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: NextFunction) {
        const requestId = req.headers['x-request-id'] || uuidv4();
        req['requestId'] = requestId;
        res.setHeader('X-Request-Id', requestId);
        next();
    }
}

Guards

Guards determine whether a request will be handled by the route handler. They are commonly used for authentication and authorization.

┌─────────────────────────────────────────────────────────────────┐
│                         GUARDS                                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Request → Guard checks → ✅ Pass → Continue to Controller      │
│                         → ❌ Fail → Return 403 Forbidden        │
│                                                                  │
│  Common Use Cases:                                               │
│  • Authentication: Is the user logged in?                       │
│  • Authorization: Does the user have permission?                │
│  • Role-based access: Is the user an admin?                     │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Creating a Guard

typescript
// src/common/guards/auth.guard.ts
import {
    Injectable,
    CanActivate,
    ExecutionContext,
    UnauthorizedException,
} from '@nestjs/common';

@Injectable()
export class AuthGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean {
        const request = context.switchToHttp().getRequest();
        const token = request.headers.authorization;

        if (!token) {
            throw new UnauthorizedException('No token provided');
        }

        // Validate token (simplified example)
        try {
            const user = this.validateToken(token);
            request.user = user;
            return true;
        } catch {
            throw new UnauthorizedException('Invalid token');
        }
    }

    private validateToken(token: string): any {
        // Token validation logic
        return { id: 1, email: 'user@example.com' };
    }
}

Applying Guards

Controller Level

typescript
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../common/guards/auth.guard';

@Controller('users')
@UseGuards(AuthGuard) // Apply to all routes in controller
export class UsersController {
    @Get()
    findAll() {
        return 'Protected route';
    }
}

Method Level

typescript
@Controller('users')
export class UsersController {
    @Get()
    findAll() {
        return 'Public route';
    }

    @Get('profile')
    @UseGuards(AuthGuard) // Apply to single route
    getProfile() {
        return 'Protected route';
    }
}

Global Level

typescript
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AuthGuard } from './common/guards/auth.guard';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    app.useGlobalGuards(new AuthGuard());
    await app.listen(3000);
}
bootstrap();

// Or with dependency injection support
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './common/guards/auth.guard';

@Module({
    providers: [
        {
            provide: APP_GUARD,
            useClass: AuthGuard,
        },
    ],
})
export class AppModule {}

Role-Based Guard

typescript
// src/common/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

// src/common/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
    constructor(private reflector: Reflector) {}

    canActivate(context: ExecutionContext): boolean {
        const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
            context.getHandler(),
            context.getClass(),
        ]);

        if (!requiredRoles) {
            return true; // No roles required, allow access
        }

        const { user } = context.switchToHttp().getRequest();
        return requiredRoles.some(role => user.roles?.includes(role));
    }
}

// Usage in controller
@Controller('admin')
@UseGuards(AuthGuard, RolesGuard)
export class AdminController {
    @Get('dashboard')
    @Roles('admin')
    getDashboard() {
        return 'Admin dashboard';
    }

    @Get('reports')
    @Roles('admin', 'manager')
    getReports() {
        return 'Reports';
    }
}

Interceptors

Interceptors can transform the result returned from a function, extend the basic function behavior, or completely override a function.

┌─────────────────────────────────────────────────────────────────┐
│                       INTERCEPTORS                               │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Request  →  Interceptor BEFORE  →  Controller  →  Interceptor AFTER  →  Response
│                  │                                       │
│                  │                                       │
│              • Log start time                      • Log end time
│              • Transform request                   • Transform response
│              • Check cache                         • Cache response
│                                                                  │
│  Key Feature: Interceptors wrap around the handler!            │
│                                                                  │
│  Common Use Cases:                                               │
│  • Logging request/response                                     │
│  • Transforming response structure                              │
│  • Caching                                                       │
│  • Timeout handling                                              │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Creating an Interceptor

typescript
// src/common/interceptors/logging.interceptor.ts
import {
    Injectable,
    NestInterceptor,
    ExecutionContext,
    CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const request = context.switchToHttp().getRequest();
        const method = request.method;
        const url = request.url;
        const now = Date.now();

        console.log(`Before: ${method} ${url}`);

        return next.handle().pipe(
            tap(() => console.log(`After: ${method} ${url} - ${Date.now() - now}ms`)),
        );
    }
}

Transform Response Interceptor

typescript
// src/common/interceptors/transform.interceptor.ts
import {
    Injectable,
    NestInterceptor,
    ExecutionContext,
    CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
    success: boolean;
    data: T;
    timestamp: string;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
    intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
        return next.handle().pipe(
            map(data => ({
                success: true,
                data,
                timestamp: new Date().toISOString(),
            })),
        );
    }
}

Cache Interceptor

typescript
// src/common/interceptors/cache.interceptor.ts
import {
    Injectable,
    NestInterceptor,
    ExecutionContext,
    CallHandler,
} from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
    private cache = new Map<string, any>();

    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        const request = context.switchToHttp().getRequest();
        const key = request.url;

        // Only cache GET requests
        if (request.method !== 'GET') {
            return next.handle();
        }

        const cachedResponse = this.cache.get(key);
        if (cachedResponse) {
            console.log(`Cache hit for ${key}`);
            return of(cachedResponse);
        }

        return next.handle().pipe(
            tap(response => {
                console.log(`Caching response for ${key}`);
                this.cache.set(key, response);
                // Clear cache after 60 seconds
                setTimeout(() => this.cache.delete(key), 60000);
            }),
        );
    }
}

Timeout Interceptor

typescript
// src/common/interceptors/timeout.interceptor.ts
import {
    Injectable,
    NestInterceptor,
    ExecutionContext,
    CallHandler,
    RequestTimeoutException,
} from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
    intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
        return next.handle().pipe(
            timeout(5000), // 5 second timeout
            catchError(err => {
                if (err instanceof TimeoutError) {
                    return throwError(() => new RequestTimeoutException());
                }
                return throwError(() => err);
            }),
        );
    }
}

Applying Interceptors

typescript
// Controller level
@Controller('users')
@UseInterceptors(LoggingInterceptor)
export class UsersController {}

// Method level
@Get()
@UseInterceptors(CacheInterceptor)
findAll() {}

// Global level (main.ts)
app.useGlobalInterceptors(new TransformInterceptor());

// Global level with DI (app.module.ts)
@Module({
    providers: [
        {
            provide: APP_INTERCEPTOR,
            useClass: TransformInterceptor,
        },
    ],
})
export class AppModule {}

Exception Filters

Exception filters handle errors thrown from your application and transform them into appropriate HTTP responses.

Built-in HTTP Exceptions

typescript
import {
    BadRequestException,
    UnauthorizedException,
    ForbiddenException,
    NotFoundException,
    ConflictException,
    InternalServerErrorException,
} from '@nestjs/common';

@Get(':id')
findOne(@Param('id') id: string) {
    const user = this.usersService.findOne(id);
    if (!user) {
        throw new NotFoundException(`User with ID ${id} not found`);
    }
    return user;
}

Creating Custom Exception Filter

typescript
// src/common/filters/http-exception.filter.ts
import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    HttpException,
    HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();
        const status = exception.getStatus();
        const exceptionResponse = exception.getResponse();

        const error =
            typeof exceptionResponse === 'string'
                ? { message: exceptionResponse }
                : (exceptionResponse as object);

        response.status(status).json({
            success: false,
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
            method: request.method,
            ...error,
        });
    }
}

Catch All Exceptions Filter

typescript
// src/common/filters/all-exceptions.filter.ts
import {
    ExceptionFilter,
    Catch,
    ArgumentsHost,
    HttpException,
    HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
    catch(exception: unknown, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const request = ctx.getRequest<Request>();

        const status =
            exception instanceof HttpException
                ? exception.getStatus()
                : HttpStatus.INTERNAL_SERVER_ERROR;

        const message =
            exception instanceof HttpException
                ? exception.message
                : 'Internal server error';

        // Log the error
        console.error('Exception:', exception);

        response.status(status).json({
            success: false,
            statusCode: status,
            timestamp: new Date().toISOString(),
            path: request.url,
            message,
        });
    }
}

Custom Exceptions

typescript
// src/common/exceptions/business.exception.ts
import { HttpException, HttpStatus } from '@nestjs/common';

export class BusinessException extends HttpException {
    constructor(message: string, errorCode: string) {
        super(
            {
                message,
                errorCode,
                error: 'Business Error',
            },
            HttpStatus.UNPROCESSABLE_ENTITY,
        );
    }
}

// Usage
throw new BusinessException('Insufficient funds', 'INSUFFICIENT_FUNDS');

Applying Exception Filters

typescript
// Controller level
@Controller('users')
@UseFilters(HttpExceptionFilter)
export class UsersController {}

// Method level
@Get()
@UseFilters(new HttpExceptionFilter())
findAll() {}

// Global level (main.ts)
app.useGlobalFilters(new AllExceptionsFilter());

// Global level with DI (app.module.ts)
@Module({
    providers: [
        {
            provide: APP_FILTER,
            useClass: AllExceptionsFilter,
        },
    ],
})
export class AppModule {}

Combining Everything

Here's an example showing all concepts working together:

typescript
// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { APP_GUARD, APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { AuthGuard } from './common/guards/auth.guard';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
import { AllExceptionsFilter } from './common/filters/all-exceptions.filter';
import { UsersModule } from './users/users.module';

@Module({
    imports: [UsersModule],
    providers: [
        // Global guard
        {
            provide: APP_GUARD,
            useClass: AuthGuard,
        },
        // Global interceptor
        {
            provide: APP_INTERCEPTOR,
            useClass: TransformInterceptor,
        },
        // Global exception filter
        {
            provide: APP_FILTER,
            useClass: AllExceptionsFilter,
        },
    ],
})
export class AppModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer.apply(LoggerMiddleware).forRoutes('*');
    }
}

Execution Order Summary

ComponentWhen ExecutedUse Case
MiddlewareFirst, before guardsLogging, CORS, body parsing
GuardsAfter middlewareAuthentication, authorization
Interceptors (pre)After guards, before handlerTransform request, caching
PipesBefore handlerValidation, transformation
HandlerMain business logicController method
Interceptors (post)After handlerTransform response
Exception FiltersWhen exception is thrownError handling

Best Practices

1. Use Guards for Authorization

typescript
// ✅ Good - Guard for auth
@UseGuards(AuthGuard, RolesGuard)
@Roles('admin')
@Delete(':id')
remove(@Param('id') id: string) {}

// ❌ Bad - Auth logic in controller
@Delete(':id')
remove(@Param('id') id: string, @Req() req: Request) {
    if (!req.user || !req.user.roles.includes('admin')) {
        throw new ForbiddenException();
    }
}

2. Use Interceptors for Response Transformation

typescript
// ✅ Good - Interceptor for consistent response
@UseInterceptors(TransformInterceptor)
@Get()
findAll() {
    return this.service.findAll();
}
// Response: { success: true, data: [...], timestamp: '...' }

// ❌ Bad - Manual wrapping in each method
@Get()
findAll() {
    const data = this.service.findAll();
    return { success: true, data, timestamp: new Date().toISOString() };
}

3. Use Exception Filters for Error Handling

typescript
// ✅ Good - Exception filter handles errors consistently
throw new NotFoundException('User not found');

// ❌ Bad - Manual error response in controller
@Get(':id')
findOne(@Param('id') id: string, @Res() res: Response) {
    const user = this.service.findOne(id);
    if (!user) {
        return res.status(404).json({ error: 'User not found' });
    }
    return res.json(user);
}

Summary

In this chapter, you learned:

  • The request lifecycle in NestJS
  • Creating and applying middleware
  • Guards for authentication and authorization
  • Interceptors for request/response transformation
  • Exception filters for error handling
  • Best practices for each component

What's Next?

In the next chapter, we'll learn about Pipes & Validation and understand:

  • Built-in pipes
  • Custom validation pipes
  • Data transformation
  • Using class-validator and class-transformer

Previous: Modules | Next: Pipes & Validation →