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
- Understanding the Request Lifecycle in NestJS
- What is Middleware in NestJS?
- What are Guards in NestJS?
- Key Differences Between Middleware and Guards
- Using Middleware for Auth
- Using Guards for Auth
- When to Use Middleware vs Guard
- Best Practices
- 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
Feature | Middleware | Guard |
---|---|---|
Execution Time | Before route handling | After route match, before controller |
Access to Route Metadata | ❌ No | ✅ Yes |
Use Cases | Logging, headers, basic parsing | Auth, RBAC, request authorization |
Return Value Behavior | No return value control flow | Must 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
Situation | Use Middleware | Use 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.