G

Guards & Interceptors

Type-safe NestJS guards for authentication/authorization and interceptors for logging/transformation.

Code

typescript
1// decorators/current-user.decorator.ts
2import { 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.ts
20import { 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.ts
27import {
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.ts
70import { 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.ts
94import {
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.ts
126import {
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: CallHandler
150 ): 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.ts
166import {
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 controller
207@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