Middleware vs Guard for Auth Handling in NestJS

When building secure applications with NestJS, authentication plays a central role. Two common mechanisms used for request-level processing are Middleware and Guards. Both can be used for handling authentication, but they serve distinct purposes and operate at different stages of the request lifecycle.

In this module, we’ll explore the key differences between middleware and guards, discuss when and how to use each for authentication, and provide real-world examples for both approaches.


Table of Contents

  1. Understanding the Request Lifecycle in NestJS
  2. What is Middleware in NestJS?
  3. What are Guards in NestJS?
  4. Key Differences Between Middleware and Guards
  5. Using Middleware for Auth
  6. Using Guards for Auth
  7. When to Use Middleware vs Guard
  8. Best Practices
  9. Conclusion

Understanding the Request Lifecycle in NestJS

To decide between middleware and guards, you need to understand where they fit into the NestJS request pipeline:

nginxCopyEditMiddleware → Guards → Interceptors → Controller → Service → Response
  • Middleware runs before route matching
  • Guards run after route matching but before controllers
  • Interceptors run around route handlers (for transformation, logging, etc.)

What is Middleware in NestJS?

Middleware functions are functions that run before your routes are handled. They are typically used for:

  • Logging
  • Request transformation
  • Attaching data to req object
  • Pre-processing headers

They are not route-aware by default and cannot access route metadata (like decorators).


What are Guards in NestJS?

Guards are classes implementing CanActivate and are used for authorization and authentication logic. They are fully aware of the route context and can use route-level metadata (e.g., roles, permissions).

Guards are great for:

  • Route-level access control
  • Protecting routes based on user roles
  • Preventing requests before reaching controllers

Key Differences Between Middleware and Guards

FeatureMiddlewareGuard
Execution TimeBefore route handlingAfter route match, before controller
Access to Route Metadata❌ No✅ Yes
Use CasesLogging, headers, basic parsingAuth, RBAC, request authorization
Return Value BehaviorNo return value control flowMust return true or throw error
Dependency Injection (DI)❌ Limited (via @Injectable workaround)✅ Full DI support

Using Middleware for Auth

Let’s say you want to attach a decoded JWT token to every request.

Step 1: Create the middleware

tsCopyEdit// auth.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import * as jwt from 'jsonwebtoken';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const token = req.headers['authorization']?.replace('Bearer ', '');

    if (token) {
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req['user'] = decoded;
      } catch (err) {
        // do not throw here — middleware shouldn’t block the request
      }
    }

    next();
  }
}

Step 2: Apply in AppModule or specific module

tsCopyEdit// app.module.ts
import { MiddlewareConsumer, Module } from '@nestjs/common';

@Module({ /* imports and providers */ })
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(AuthMiddleware).forRoutes('*');
  }
}

Middleware cannot prevent route execution, which is a critical limitation for secure auth.


Using Guards for Auth

For secure route protection, use Guards with dependency injection.

Step 1: Create a guard

tsCopyEdit// jwt-auth.guard.ts
import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import * as jwt from 'jsonwebtoken';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const token = request.headers['authorization']?.replace('Bearer ', '');

    if (!token) {
      throw new UnauthorizedException('Token missing');
    }

    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      request.user = decoded;
      return true;
    } catch (err) {
      throw new UnauthorizedException('Token invalid');
    }
  }
}

Step 2: Apply the guard to routes

tsCopyEdit@UseGuards(JwtAuthGuard)
@Get('profile')
getProfile(@Req() req) {
  return req.user;
}

When to Use Middleware vs Guard

SituationUse MiddlewareUse Guard
You need to parse token and attach to req
You need to protect a route
You need to read route decorators (roles)
You want to log all incoming requests
You want to apply global auth filter✅ (light) + ✅ Guard

Best Practices

  • Use middleware for lightweight pre-processing (like decoding JWTs, logging).
  • Use guards for anything related to authorization, roles, or blocking requests.
  • Use combination: middleware can decode the token, guard can verify access based on route metadata.
  • Avoid throwing exceptions inside middleware — handle errors gracefully.

Conclusion

Both middleware and guards are powerful tools in NestJS, but they solve different problems. Middleware is ideal for preprocessing, while guards provide route-aware access control. For robust authentication systems, especially where roles and permissions are involved, guards are the go-to mechanism.