G
Guards & Interceptors
Type-safe NestJS guards for authentication/authorization and interceptors for logging/transformation.
Code
typescript
1// decorators/current-user.decorator.ts2import { createParamDecorator, ExecutionContext } from "@nestjs/common";3 4export interface CurrentUser {5 id: number;6 email: string;7 role: "admin" | "user";8}9 10export const CurrentUser = createParamDecorator(11 (data: keyof CurrentUser | undefined, ctx: ExecutionContext): CurrentUser | unknown => {12 const request = ctx.switchToHttp().getRequest();13 const user = request.user as CurrentUser;14 15 return data ? user?.[data] : user;16 }17);18 19// decorators/roles.decorator.ts20import { SetMetadata } from "@nestjs/common";21 22export type Role = "admin" | "user";23export const ROLES_KEY = "roles";24export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);25 26// guards/jwt-auth.guard.ts27import {28 Injectable,29 CanActivate,30 ExecutionContext,31 UnauthorizedException,32} from "@nestjs/common";33import { JwtService } from "@nestjs/jwt";34 35@Injectable()36export class JwtAuthGuard implements CanActivate {37 constructor(private jwtService: JwtService) {}38 39 async canActivate(context: ExecutionContext): Promise<boolean> {40 const request = context.switchToHttp().getRequest();41 const token = this.extractToken(request);42 43 if (!token) {44 throw new UnauthorizedException("No token provided");45 }46 47 try {48 const payload = await this.jwtService.verifyAsync(token);49 request.user = {50 id: payload.sub,51 email: payload.email,52 role: payload.role,53 };54 return true;55 } catch {56 throw new UnauthorizedException("Invalid token");57 }58 }59 60 private extractToken(request: Request): string | undefined {61 const authHeader = request.headers["authorization"];62 if (!authHeader) return undefined;63 64 const [type, token] = authHeader.split(" ");65 return type === "Bearer" ? token : undefined;66 }67}68 69// guards/roles.guard.ts70import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common";71import { Reflector } from "@nestjs/core";72import { Role, ROLES_KEY } from "../decorators/roles.decorator";73 74@Injectable()75export class RolesGuard implements CanActivate {76 constructor(private reflector: Reflector) {}77 78 canActivate(context: ExecutionContext): boolean {79 const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [80 context.getHandler(),81 context.getClass(),82 ]);83 84 if (!requiredRoles) {85 return true;86 }87 88 const { user } = context.switchToHttp().getRequest();89 return requiredRoles.includes(user.role);90 }91}92 93// interceptors/logging.interceptor.ts94import {95 Injectable,96 NestInterceptor,97 ExecutionContext,98 CallHandler,99 Logger,100} from "@nestjs/common";101import { Observable } from "rxjs";102import { tap } from "rxjs/operators";103 104@Injectable()105export class LoggingInterceptor implements NestInterceptor {106 private readonly logger = new Logger(LoggingInterceptor.name);107 108 intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {109 const request = context.switchToHttp().getRequest();110 const { method, url } = request;111 const now = Date.now();112 113 return next.handle().pipe(114 tap(() => {115 const response = context.switchToHttp().getResponse();116 const { statusCode } = response;117 const duration = Date.now() - now;118 119 this.logger.log(`${method} ${url} ${statusCode} - ${duration}ms`);120 })121 );122 }123}124 125// interceptors/transform.interceptor.ts126import {127 Injectable,128 NestInterceptor,129 ExecutionContext,130 CallHandler,131} from "@nestjs/common";132import { Observable } from "rxjs";133import { map } from "rxjs/operators";134 135export interface ApiResponse<T> {136 data: T;137 meta: {138 timestamp: string;139 path: string;140 };141}142 143@Injectable()144export class TransformInterceptor<T>145 implements NestInterceptor<T, ApiResponse<T>>146{147 intercept(148 context: ExecutionContext,149 next: CallHandler150 ): Observable<ApiResponse<T>> {151 const request = context.switchToHttp().getRequest();152 153 return next.handle().pipe(154 map((data) => ({155 data,156 meta: {157 timestamp: new Date().toISOString(),158 path: request.url,159 },160 }))161 );162 }163}164 165// interceptors/cache.interceptor.ts166import {167 Injectable,168 NestInterceptor,169 ExecutionContext,170 CallHandler,171} from "@nestjs/common";172import { Observable, of } from "rxjs";173import { tap } from "rxjs/operators";174 175@Injectable()176export class CacheInterceptor implements NestInterceptor {177 private cache = new Map<string, { data: unknown; expiry: number }>();178 179 constructor(private ttl: number = 60000) {}180 181 intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {182 const request = context.switchToHttp().getRequest();183 184 if (request.method !== "GET") {185 return next.handle();186 }187 188 const key = request.url;189 const cached = this.cache.get(key);190 191 if (cached && cached.expiry > Date.now()) {192 return of(cached.data);193 }194 195 return next.handle().pipe(196 tap((data) => {197 this.cache.set(key, {198 data,199 expiry: Date.now() + this.ttl,200 });201 })202 );203 }204}205 206// Usage in controller207@Controller("users")208@UseGuards(JwtAuthGuard, RolesGuard)209@UseInterceptors(LoggingInterceptor, TransformInterceptor)210export class UserController {211 @Get("profile")212 getProfile(@CurrentUser() user: CurrentUser) {213 return user;214 }215 216 @Get("admin")217 @Roles("admin")218 getAdminData() {219 return { secret: "admin only data" };220 }221}Run this example locally
$ npm install @nestjs/jwt @nestjs/passport class-validator class-transformer