NestJS Day 6: What are Guards and Interceptors?
Abstract
This is a concise, summarized approach to learn NestJS. For more in-depth knowledge about NestJS, visit the official NestJS documentation.
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
@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
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
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
@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
@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
@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
@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
@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
@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 requestexport class ResourceController { @Get() @UseGuards(RolesGuard) // 3. Check roles @UseInterceptors(CacheInterceptor) // 4. Check cache findAll() { return this.resourceService.findAll(); }}
2. Global Guards and Interceptors
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 guardsconst AuthAndRoles = (...roles: string[]) => applyDecorators( UseGuards(AuthGuard), Roles(...roles), UseGuards(RolesGuard));
// Usage@Get('admin-only')@AuthAndRoles('admin')adminEndpoint() { return 'admin data';}
// Compose interceptorsconst ApiResponse = () => applyDecorators( UseInterceptors(TransformInterceptor), UseInterceptors(CacheInterceptor), UseInterceptors(LoggingInterceptor));
// Usage@Get()@ApiResponse()findAll() { return this.service.findAll();}
Testing Guards and Interceptors
Testing Guards
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
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