What are guards in NestJS and how to implement authentication?
Guards in NestJS are classes that implement CanActivate and run before route handlers to decide whether a request can proceed, based on authentication status or user permissions.
Theory
TL;DR
- Think of a guard as a bouncer: it checks the token at the door before the request reaches your controller; no valid token, no entry
canActivatereturnstrueto allow the request or throws an exception to block it; returningfalseproduces a 403- Guards run after middleware but before interceptors, pipes, and the route handler
- Apply with
@UseGuards()on a method, a controller class, or globally viaAPP_GUARD - Authentication (who are you?) and authorization (what can you do?) are separate concerns; chain two guards to handle both
Quick example
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
if (!request.headers.authorization) {
throw new UnauthorizedException('No token');
}
return true; // request reaches the route handler
}
}
// Protect a single route
@Get('profile')
@UseGuards(AuthGuard)
getProfile() { return 'protected data'; }GET /profile without an Authorization header returns 401. With the header present, the controller runs.
How guards fit in the pipeline
NestJS processes each request in this order: middleware, guards, interceptors, pipes, route handler, exception filters. Guards sit at position two. They see the request after any middleware parsing and stop it before your business logic runs.
If canActivate returns false or throws, the pipeline stops there. The exception filter catches the thrown error and formats the HTTP response. Your controller code never executes.
The ExecutionContext object is what makes guards more capable than middleware. It wraps the underlying platform (Express or Fastify) and exposes context.switchToHttp().getRequest() for the raw request, plus context.getHandler() and context.getClass() for reading metadata set by custom decorators like @Roles('admin').
When to use guards
- One route needs auth:
@UseGuards(JwtAuthGuard)on the method - Whole controller needs auth:
@UseGuards(JwtAuthGuard)on the class - Entire app protected by default: register via
APP_GUARDin a module (this supports DI, unlikeapp.useGlobalGuards()inmain.ts) - Some routes must bypass auth in a globally guarded app: a
@Public()decorator pattern handles this cleanly - Role-based access on top of identity checks: chain
@UseGuards(JwtAuthGuard, RolesGuard)
Guard vs Middleware vs Interceptor
| Guard | Middleware | Interceptor | |
|---|---|---|---|
Has ExecutionContext | Yes | No | Yes |
| Can read route metadata | Yes | No | Yes |
| Runs before handler | Yes | Yes | Yes (pre-phase) |
| Can block request | Yes | Yes | Yes |
| Main purpose | Auth, authorization | Parsing, CORS, logging | Response transform, logging |
Middleware works at the Express/Fastify layer with no knowledge of NestJS routes or metadata. Guards run inside NestJS and can inspect exactly which controller and method the request is targeting. Use middleware for global HTTP concerns like cookie parsing. Use guards for access decisions.
Common mistakes
Mistake 1: Not using async with jwtService.verifyAsync
// Wrong - Promise is ignored, guard always returns true
canActivate(context: ExecutionContext): boolean {
this.jwtService.verifyAsync(token); // never awaited
return true;
}
// Correct
async canActivate(context: ExecutionContext): Promise<boolean> {
const payload = await this.jwtService.verifyAsync(token);
request.user = payload;
return true;
}Mistake 2: Skipping request.user = payload
The guard validates the token, but the controller still needs the decoded data. Without attaching the payload to request.user, every handler that reads req.user gets undefined. This shows up as a silent production bug and is easy to miss when you mock the guard in tests.
Mistake 3: Forgetting @Injectable()
Without @Injectable(), NestJS cannot instantiate the guard through its DI container. The app crashes at startup with a confusing DI error.
Mistake 4: Throwing new Error() instead of UnauthorizedException
new Error() bypasses the NestJS exception filter. The response comes back as a 500 with an HTML body instead of a clean 401 JSON object. Use UnauthorizedException for 401 and ForbiddenException for 403.
Mistake 5: Global guard blocking public routes
A globally registered JWT guard will require a token on /health, /auth/login, and /auth/register. Add the @Public() decorator pattern so those routes skip the token check.
Real-world usage
- NestJS + Prisma: guard checks
req.user.idagainst the resource owner before allowing writes or deletes - GraphQL resolvers:
@UseGuards()works on resolvers the same way as on REST controllers; callcontext.switchToGraphql()instead ofswitchToHttp() - Microservices:
JwtAuthGuardregistered globally, only auth endpoints carry@Public() - Fastify adapter: guard code stays identical;
switchToHttp()abstracts the platform difference internally
Follow-up questions
Q: What is the full execution order in NestJS?
A: Middleware runs first. Then guards. Then interceptors (before handler). Then pipes. Then the route handler. Then interceptors again (after handler). Exception filters run last if anything throws.
Q: How do you chain multiple guards?
A: @UseGuards(Guard1, Guard2). All guards must return true. NestJS stops at the first guard that returns false or throws.
Q: What is the difference between an auth guard and a roles guard?
A: Auth guard checks identity: is this token valid and does a user exist? Roles guard checks permissions: does this user have the required role? They handle different questions and are typically chained.
Q: How do you test a guard in isolation?
A: Create a testing module with the guard as a provider. Mock ExecutionContext as a plain object: { switchToHttp: () => ({ getRequest: () => mockReq }) }. Assert canActivate returns true for valid input and throws for invalid.
Q (Senior): In a globally registered JwtAuthGuard, how do you expose /auth/login without a token?
A: Create a @Public() decorator using SetMetadata('isPublic', true). In the guard, use Reflector.getAllAndOverride to check for that flag before running token validation. If the route is marked public, return true immediately and skip the JWT check entirely.
Examples
Basic AuthGuard
The simplest guard checks for any Authorization header. Good starting point before adding JWT logic.
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
if (!request.headers.authorization) {
throw new UnauthorizedException('Authorization header is missing');
}
return true; // any header value passes; real apps validate the token here
}
}@Injectable() is required. The guard throws UnauthorizedException (401) when the header is missing and returns true otherwise. Any subsequent guard in the chain runs only if this one passes.
JWT Auth Guard (production pattern)
This guard validates a Bearer token using @nestjs/jwt, attaches the decoded payload to request.user, and handles expired or malformed tokens with a clean 401.
// guards/jwt-auth.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
interface AuthRequest extends Request {
user?: Record<string, unknown>;
}
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<AuthRequest>();
const token = this.extractToken(request);
if (!token) throw new UnauthorizedException('No Bearer token');
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});
request.user = payload; // available as req.user in any controller
} catch {
throw new UnauthorizedException('Invalid or expired token');
}
return true;
}
private extractToken(request: AuthRequest): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// users.controller.ts
@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
@Get('profile')
getProfile(@Req() req: AuthRequest) {
return req.user; // { sub: 'userId', email: '...', iat: ... }
}
}Valid Bearer token returns 200 with the decoded payload. Expired or tampered token returns 401. The async/await on verifyAsync is not optional; skipping it means the guard always passes regardless of token validity.
Roles Guard with @Public() decorator
This covers two production patterns together: role-based access using Reflector and marking routes public in a globally guarded app. I have seen the missing @Public() on login routes cause real confusion in teams switching from method-level to global guards.
// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// 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(), // method-level metadata checked first
context.getClass(), // then controller-level
]);
if (!requiredRoles) return true; // no @Roles() on this route, allow through
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// admin.controller.ts
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard) // JWT validates identity first, then roles
export class AdminController {
@Get('users')
@Roles('admin')
getAllUsers() {
return 'admin only';
}
}
// auth.controller.ts - must be public when JwtAuthGuard is registered globally
@Public()
@Post('auth/login')
login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
// app.module.ts - global registration with full DI support
@Module({
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: RolesGuard },
],
})
export class AppModule {}A user with roles: ['admin'] in the JWT payload passes both guards. A user with roles: ['viewer'] passes JWT validation but gets a 403 from RolesGuard. The @Public() decorator on the login route bypasses the JWT check entirely, so unauthenticated users can still get a token.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.