Skip to content

NestJS Day 6: What are Guards and Interceptors?

NestJS Day 6

Abstract

This is a concise, summarized approach to learn NestJS. For more in-depth knowledge about NestJS, visit the official NestJS documentation.

Github repo for day 6

HAR file

Introduction to Guards

Guards in NestJS determine whether a request should be handled by the route handler based on certain conditions (permissions, roles, etc.).

Basic Guard Structure

import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";
@Injectable()
export class BasicGuard implements CanActivate {
// Must implement canActivate method
canActivate(context: ExecutionContext): boolean | Promise<boolean> {
// Get request object
const request = context.switchToHttp().getRequest();
// Return true to allow, false to deny
return true;
}
}

Authentication Guard Example

auth.guard.ts
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// Get token from headers
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException("No token provided");
}
try {
// Verify JWT token
const payload = await this.jwtService.verifyAsync(token);
// Attach user to request object for later use
request["user"] = payload;
return true;
} catch {
throw new UnauthorizedException("Invalid token");
}
}
private extractTokenFromHeader(request: Request): string | undefined {
// Bearer <token>
const [type, token] = request.headers.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}

Role-based Guard

roles.decorator.ts
export const ROLES_KEY = "roles";
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
// roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// Get roles from decorator
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()]
);
if (!requiredRoles) {
return true; // No roles required
}
const { user } = context.switchToHttp().getRequest();
// Check if user has required role
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// Usage in controller
@Controller("cats")
export class CatsController {
@Get()
@Roles("admin") // Only admin can access
@UseGuards(AuthGuard, RolesGuard) // Check auth first, then roles
findAll() {
return "All cats";
}
}

Advanced Guard with Metadata

permission.decorator.ts
export const PERMISSIONS_KEY = 'permissions';
export const RequirePermissions = (...permissions: string[]) =>
SetMetadata(PERMISSIONS_KEY, permissions);
// permission.guard.ts
@Injectable()
export class PermissionGuard implements CanActivate {
constructor(
private reflector: Reflector,
private userService: UserService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// Get permissions from decorator
const requiredPermissions = this.reflector.get<string[]>(
PERMISSIONS_KEY,
context.getHandler(),
);
if (!requiredPermissions) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
// Get user permissions from database
const userPermissions = await this.userService.getUserPermissions(user.id);
return requiredPermissions.every(
permission => userPermissions.includes(permission)
);
}
}
// Usage
@Get('sensitive-data')
@RequirePermissions('read:data', 'export:data')
@UseGuards(AuthGuard, PermissionGuard)
getSensitiveData() {
return 'sensitive data';
}

Introduction to Interceptors

Interceptors add extra logic before and after method execution (logging, transforming response, etc.).

Basic Interceptor Structure

import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
@Injectable()
export class BasicInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// Logic before method execution
console.log("Before...");
return next.handle().pipe(tap(() => console.log("After...")));
}
}

Response Transformation Interceptor

transform.interceptor.ts
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>>
{
intercept(
context: ExecutionContext,
next: CallHandler
): Observable<Response<T>> {
// Get request timestamp
const now = Date.now();
return next.handle().pipe(
map((data) => ({
data,
statusCode: context.switchToHttp().getResponse().statusCode,
timestamp: now,
requestId: context.switchToHttp().getRequest().id,
}))
);
}
}
// Usage
@Controller("cats")
@UseInterceptors(TransformInterceptor)
export class CatsController {
@Get()
findAll() {
return ["cat1", "cat2"];
}
}
// Response:
// {
// "data": ["cat1", "cat2"],
// "statusCode": 200,
// "timestamp": 1678111111111,
// "requestId": "123"
// }

Caching Interceptor

cache.interceptor.ts
@Injectable()
export class CacheInterceptor implements NestInterceptor {
constructor(private cacheManager: Cache) {}
async intercept(
context: ExecutionContext,
next: CallHandler
): Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const cacheKey = request.url;
// Try to get from cache
const cachedResponse = await this.cacheManager.get(cacheKey);
if (cachedResponse) {
return of(cachedResponse);
}
// If not in cache, execute handler and cache result
return next.handle().pipe(
tap(async (response) => {
await this.cacheManager.set(cacheKey, response, { ttl: 60 }); // Cache for 60s
})
);
}
}
// Usage with specific routes
@Controller("products")
export class ProductsController {
@Get()
@UseInterceptors(CacheInterceptor)
async findAll() {
// Expensive database query
return this.productsService.findAll();
}
}

Logging Interceptor with Timing

logging.interceptor.ts
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
constructor(private logger: Logger) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
const now = Date.now();
// Log request
this.logger.log(`${method} ${url}`);
return next.handle().pipe(
tap({
next: (val) => {
// Log successful response
this.logger.log(
`${method} ${url} ${Date.now() - now}ms`,
JSON.stringify(val)
);
},
error: (err) => {
// Log error
this.logger.error(
`${method} ${url} ${Date.now() - now}ms`,
err.stack
);
},
})
);
}
}

Exception Handling Interceptor

exception.interceptor.ts
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((err) => {
// Transform error response
if (err instanceof HttpException) {
return throwError(() => err);
}
// Log unexpected errors
console.error(err);
// Return standardized error
return throwError(
() => new InternalServerErrorException("Something went wrong")
);
})
);
}
}

Timeout Interceptor

timeout.interceptor.ts
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
constructor(private readonly timeout = 5000) {} // 5s default
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
// Cancel request if it takes too long
timeout(this.timeout),
catchError((err) => {
if (err instanceof TimeoutError) {
throw new RequestTimeoutException("Request took too long");
}
throw err;
})
);
}
}
// Usage
@Controller("slow-endpoint")
export class SlowController {
@Get()
@UseInterceptors(new TimeoutInterceptor(10000)) // 10s timeout
async slowOperation() {
await someSlowOperation();
return "Done";
}
}

File Upload Interceptor

file-upload.interceptor.ts
@Injectable()
export class FileUploadInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// Check if file exists
if (!request.file) {
throw new BadRequestException('File is required');
}
// Validate file type
const allowedMimes = ['image/jpeg', 'image/png'];
if (!allowedMimes.includes(request.file.mimetype)) {
throw new BadRequestException('Invalid file type');
}
// Add file metadata
request.file.metadata = {
uploadedAt: new Date(),
originalName: request.file.originalname,
};
return next.handle();
}
}
// Usage with multer
@Post('upload')
@UseInterceptors(
FileInterceptor('file'),
FileUploadInterceptor
)
uploadFile(@UploadedFile() file: Express.Multer.File) {
return {
filename: file.filename,
metadata: file.metadata,
};
}

Best Practices

1. Guard and Interceptor Execution Order

@Controller("resources")
@UseGuards(AuthGuard) // 1. Authenticate first
@UseInterceptors(LoggingInterceptor) // 2. Log request
export class ResourceController {
@Get()
@UseGuards(RolesGuard) // 3. Check roles
@UseInterceptors(CacheInterceptor) // 4. Check cache
findAll() {
return this.resourceService.findAll();
}
}

2. Global Guards and Interceptors

main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Global guards
app.useGlobalGuards(new AuthGuard());
// Global interceptors
app.useGlobalInterceptors(
new LoggingInterceptor(),
new TransformInterceptor()
);
await app.listen(3000);
}
// Or using dependency injection (preferred)
@Module({
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
export class AppModule {}

3. Composing Guards and Interceptors

// Compose multiple guards
const AuthAndRoles = (...roles: string[]) => applyDecorators(
UseGuards(AuthGuard),
Roles(...roles),
UseGuards(RolesGuard)
);
// Usage
@Get('admin-only')
@AuthAndRoles('admin')
adminEndpoint() {
return 'admin data';
}
// Compose interceptors
const ApiResponse = () => applyDecorators(
UseInterceptors(TransformInterceptor),
UseInterceptors(CacheInterceptor),
UseInterceptors(LoggingInterceptor)
);
// Usage
@Get()
@ApiResponse()
findAll() {
return this.service.findAll();
}

Testing Guards and Interceptors

Testing Guards

auth.guard.spec.ts
describe("AuthGuard", () => {
let guard: AuthGuard;
let jwtService: JwtService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
AuthGuard,
{
provide: JwtService,
useValue: {
verifyAsync: jest.fn(),
},
},
],
}).compile();
guard = module.get<AuthGuard>(AuthGuard);
jwtService = module.get<JwtService>(JwtService);
});
it("should pass with valid token", async () => {
const context = createMock<ExecutionContext>();
const request = {
headers: {
authorization: "Bearer valid-token",
},
};
context.switchToHttp().getRequest.mockReturnValue(request);
jest.spyOn(jwtService, "verifyAsync").mockResolvedValue({ id: 1 });
expect(await guard.canActivate(context)).toBe(true);
expect(request["user"]).toEqual({ id: 1 });
});
});

Testing Interceptors

transform.interceptor.spec.ts
describe("TransformInterceptor", () => {
let interceptor: TransformInterceptor<any>;
beforeEach(() => {
interceptor = new TransformInterceptor();
});
it("should transform response", (done) => {
const context = createMock<ExecutionContext>();
const next = createMock<CallHandler>();
context.switchToHttp().getResponse.mockReturnValue({ statusCode: 200 });
next.handle.mockReturnValue(of(["test"]));
interceptor.intercept(context, next).subscribe({
next: (value) => {
expect(value).toHaveProperty("data", ["test"]);
expect(value).toHaveProperty("statusCode", 200);
expect(value).toHaveProperty("timestamp");
done();
},
});
});
});

Summary

  • Guards:

    • Control access to routes
    • Run before request reaches handler
    • Perfect for authentication and authorization
  • Interceptors:

    • Transform request/response
    • Add logging, caching, timing
    • Handle errors globally
    • Run before and after request handler
  • Best Practices:

    • Use guards for security
    • Use interceptors for cross-cutting concerns
    • Combine for complex workflows
    • Consider execution order
    • Test thoroughly