@malamute/ai-rules 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +272 -121
  2. package/bin/cli.js +5 -2
  3. package/configs/_shared/CLAUDE.md +52 -149
  4. package/configs/_shared/rules/conventions/documentation.md +324 -0
  5. package/configs/_shared/rules/conventions/git.md +265 -0
  6. package/configs/_shared/rules/conventions/npm.md +80 -0
  7. package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
  8. package/configs/_shared/rules/conventions/principles.md +334 -0
  9. package/configs/_shared/rules/devops/ci-cd.md +262 -0
  10. package/configs/_shared/rules/devops/docker.md +275 -0
  11. package/configs/_shared/rules/devops/nx.md +194 -0
  12. package/configs/_shared/rules/domain/backend/api-design.md +203 -0
  13. package/configs/_shared/rules/lang/csharp/async.md +220 -0
  14. package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
  15. package/configs/_shared/rules/lang/csharp/linq.md +210 -0
  16. package/configs/_shared/rules/lang/python/async.md +337 -0
  17. package/configs/_shared/rules/lang/python/celery.md +476 -0
  18. package/configs/_shared/rules/lang/python/config.md +339 -0
  19. package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
  20. package/configs/_shared/rules/lang/python/deployment.md +523 -0
  21. package/configs/_shared/rules/lang/python/error-handling.md +330 -0
  22. package/configs/_shared/rules/lang/python/migrations.md +421 -0
  23. package/configs/_shared/rules/lang/python/python.md +172 -0
  24. package/configs/_shared/rules/lang/python/repository.md +383 -0
  25. package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
  26. package/configs/_shared/rules/lang/typescript/async.md +447 -0
  27. package/configs/_shared/rules/lang/typescript/generics.md +356 -0
  28. package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
  29. package/configs/_shared/rules/quality/error-handling.md +48 -0
  30. package/configs/_shared/rules/quality/logging.md +45 -0
  31. package/configs/_shared/rules/quality/observability.md +240 -0
  32. package/configs/_shared/rules/quality/testing-patterns.md +65 -0
  33. package/configs/_shared/rules/security/secrets-management.md +222 -0
  34. package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
  35. package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
  36. package/configs/_shared/skills/dev/api-endpoint/SKILL.md +126 -0
  37. package/configs/_shared/{.claude/commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
  38. package/configs/_shared/{.claude/commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
  39. package/configs/_shared/{.claude/commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
  40. package/configs/_shared/skills/infra/deploy/SKILL.md +139 -0
  41. package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
  42. package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
  43. package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
  44. package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
  45. package/configs/angular/CLAUDE.md +24 -216
  46. package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
  47. package/configs/angular/rules/core/resource.md +285 -0
  48. package/configs/angular/rules/core/signals.md +323 -0
  49. package/configs/angular/rules/http.md +338 -0
  50. package/configs/angular/rules/routing.md +291 -0
  51. package/configs/angular/rules/ssr.md +312 -0
  52. package/configs/angular/rules/state/signal-store.md +408 -0
  53. package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
  54. package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
  55. package/configs/angular/rules/ui/aria.md +422 -0
  56. package/configs/angular/rules/ui/forms.md +424 -0
  57. package/configs/angular/rules/ui/pipes-directives.md +335 -0
  58. package/configs/angular/{.claude/settings.json → settings.json} +3 -0
  59. package/configs/dotnet/CLAUDE.md +53 -286
  60. package/configs/dotnet/rules/background-services.md +552 -0
  61. package/configs/dotnet/rules/configuration.md +426 -0
  62. package/configs/dotnet/rules/ddd.md +447 -0
  63. package/configs/dotnet/rules/dependency-injection.md +343 -0
  64. package/configs/dotnet/rules/mediatr.md +320 -0
  65. package/configs/dotnet/rules/middleware.md +489 -0
  66. package/configs/dotnet/rules/result-pattern.md +363 -0
  67. package/configs/dotnet/rules/validation.md +388 -0
  68. package/configs/dotnet/settings.json +29 -0
  69. package/configs/fastapi/CLAUDE.md +144 -0
  70. package/configs/fastapi/rules/background-tasks.md +254 -0
  71. package/configs/fastapi/rules/dependencies.md +170 -0
  72. package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
  73. package/configs/fastapi/rules/lifespan.md +274 -0
  74. package/configs/fastapi/rules/middleware.md +229 -0
  75. package/configs/fastapi/rules/pydantic.md +433 -0
  76. package/configs/fastapi/rules/responses.md +251 -0
  77. package/configs/fastapi/rules/routers.md +202 -0
  78. package/configs/fastapi/rules/security.md +222 -0
  79. package/configs/fastapi/rules/testing.md +251 -0
  80. package/configs/fastapi/rules/websockets.md +298 -0
  81. package/configs/fastapi/settings.json +35 -0
  82. package/configs/flask/CLAUDE.md +166 -0
  83. package/configs/flask/rules/blueprints.md +208 -0
  84. package/configs/flask/rules/cli.md +285 -0
  85. package/configs/flask/rules/configuration.md +281 -0
  86. package/configs/flask/rules/context.md +238 -0
  87. package/configs/flask/rules/error-handlers.md +278 -0
  88. package/configs/flask/rules/extensions.md +278 -0
  89. package/configs/flask/rules/flask.md +171 -0
  90. package/configs/flask/rules/marshmallow.md +206 -0
  91. package/configs/flask/rules/security.md +267 -0
  92. package/configs/flask/rules/testing.md +284 -0
  93. package/configs/flask/settings.json +35 -0
  94. package/configs/nestjs/CLAUDE.md +57 -215
  95. package/configs/nestjs/rules/common-patterns.md +300 -0
  96. package/configs/nestjs/rules/filters.md +376 -0
  97. package/configs/nestjs/rules/interceptors.md +317 -0
  98. package/configs/nestjs/rules/middleware.md +321 -0
  99. package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
  100. package/configs/nestjs/rules/pipes.md +351 -0
  101. package/configs/nestjs/rules/websockets.md +451 -0
  102. package/configs/nestjs/settings.json +31 -0
  103. package/configs/nextjs/CLAUDE.md +69 -331
  104. package/configs/nextjs/rules/api-routes.md +358 -0
  105. package/configs/nextjs/rules/authentication.md +355 -0
  106. package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
  107. package/configs/nextjs/rules/data-fetching.md +249 -0
  108. package/configs/nextjs/rules/database.md +400 -0
  109. package/configs/nextjs/rules/middleware.md +303 -0
  110. package/configs/nextjs/rules/routing.md +324 -0
  111. package/configs/nextjs/rules/seo.md +350 -0
  112. package/configs/nextjs/rules/server-actions.md +353 -0
  113. package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
  114. package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
  115. package/package.json +24 -9
  116. package/src/cli.js +218 -0
  117. package/src/config.js +63 -0
  118. package/src/index.js +4 -0
  119. package/src/installer.js +414 -0
  120. package/src/merge.js +109 -0
  121. package/src/tech-config.json +45 -0
  122. package/src/utils.js +88 -0
  123. package/configs/dotnet/.claude/settings.json +0 -9
  124. package/configs/nestjs/.claude/settings.json +0 -15
  125. package/configs/python/.claude/rules/flask.md +0 -332
  126. package/configs/python/.claude/settings.json +0 -18
  127. package/configs/python/CLAUDE.md +0 -273
  128. package/src/install.js +0 -315
  129. /package/configs/_shared/{.claude/rules → rules/domain/frontend}/accessibility.md +0 -0
  130. /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
  131. /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
  132. /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
  133. /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
  134. /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
  135. /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
  136. /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
  137. /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
  138. /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
  139. /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
  140. /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
  141. /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
  142. /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
  143. /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
  144. /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
  145. /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
@@ -0,0 +1,376 @@
1
+ ---
2
+ paths:
3
+ - "**/*.filter.ts"
4
+ - "**/filters/**/*.ts"
5
+ ---
6
+
7
+ # NestJS Exception Filters
8
+
9
+ ## Built-in Exceptions
10
+
11
+ ```typescript
12
+ import {
13
+ BadRequestException,
14
+ UnauthorizedException,
15
+ ForbiddenException,
16
+ NotFoundException,
17
+ ConflictException,
18
+ GoneException,
19
+ PayloadTooLargeException,
20
+ UnsupportedMediaTypeException,
21
+ UnprocessableEntityException,
22
+ InternalServerErrorException,
23
+ NotImplementedException,
24
+ BadGatewayException,
25
+ ServiceUnavailableException,
26
+ GatewayTimeoutException,
27
+ } from '@nestjs/common';
28
+
29
+ // Usage with message
30
+ throw new NotFoundException('User not found');
31
+
32
+ // Usage with object
33
+ throw new BadRequestException({
34
+ message: 'Validation failed',
35
+ errors: [{ field: 'email', message: 'Invalid email format' }],
36
+ });
37
+ ```
38
+
39
+ ## Global Exception Filter
40
+
41
+ ```typescript
42
+ // filters/all-exceptions.filter.ts
43
+ import {
44
+ ExceptionFilter,
45
+ Catch,
46
+ ArgumentsHost,
47
+ HttpException,
48
+ HttpStatus,
49
+ Logger,
50
+ } from '@nestjs/common';
51
+ import { Request, Response } from 'express';
52
+
53
+ @Catch()
54
+ export class AllExceptionsFilter implements ExceptionFilter {
55
+ private readonly logger = new Logger(AllExceptionsFilter.name);
56
+
57
+ catch(exception: unknown, host: ArgumentsHost) {
58
+ const ctx = host.switchToHttp();
59
+ const response = ctx.getResponse<Response>();
60
+ const request = ctx.getRequest<Request>();
61
+
62
+ const status =
63
+ exception instanceof HttpException
64
+ ? exception.getStatus()
65
+ : HttpStatus.INTERNAL_SERVER_ERROR;
66
+
67
+ const message =
68
+ exception instanceof HttpException
69
+ ? exception.getResponse()
70
+ : 'Internal server error';
71
+
72
+ const errorResponse = {
73
+ statusCode: status,
74
+ timestamp: new Date().toISOString(),
75
+ path: request.url,
76
+ method: request.method,
77
+ message: typeof message === 'string' ? message : (message as Record<string, unknown>).message,
78
+ ...(typeof message === 'object' && message !== null
79
+ ? { details: message }
80
+ : {}),
81
+ };
82
+
83
+ // Log error
84
+ this.logger.error(
85
+ `${request.method} ${request.url} ${status}`,
86
+ exception instanceof Error ? exception.stack : undefined,
87
+ );
88
+
89
+ response.status(status).json(errorResponse);
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## HTTP Exception Filter
95
+
96
+ ```typescript
97
+ // filters/http-exception.filter.ts
98
+ import {
99
+ ExceptionFilter,
100
+ Catch,
101
+ ArgumentsHost,
102
+ HttpException,
103
+ } from '@nestjs/common';
104
+ import { Request, Response } from 'express';
105
+
106
+ @Catch(HttpException)
107
+ export class HttpExceptionFilter implements ExceptionFilter {
108
+ catch(exception: HttpException, host: ArgumentsHost) {
109
+ const ctx = host.switchToHttp();
110
+ const response = ctx.getResponse<Response>();
111
+ const request = ctx.getRequest<Request>();
112
+ const status = exception.getStatus();
113
+ const exceptionResponse = exception.getResponse();
114
+
115
+ response.status(status).json({
116
+ statusCode: status,
117
+ timestamp: new Date().toISOString(),
118
+ path: request.url,
119
+ ...(typeof exceptionResponse === 'object'
120
+ ? exceptionResponse
121
+ : { message: exceptionResponse }),
122
+ });
123
+ }
124
+ }
125
+ ```
126
+
127
+ ## Custom Exception Classes
128
+
129
+ ```typescript
130
+ // exceptions/business.exception.ts
131
+ import { HttpException, HttpStatus } from '@nestjs/common';
132
+
133
+ export class BusinessException extends HttpException {
134
+ constructor(
135
+ public readonly code: string,
136
+ message: string,
137
+ status: HttpStatus = HttpStatus.BAD_REQUEST,
138
+ ) {
139
+ super({ code, message }, status);
140
+ }
141
+ }
142
+
143
+ // exceptions/domain-exceptions.ts
144
+ export class UserNotFoundException extends BusinessException {
145
+ constructor(userId: string) {
146
+ super('USER_NOT_FOUND', `User with ID ${userId} not found`, HttpStatus.NOT_FOUND);
147
+ }
148
+ }
149
+
150
+ export class InsufficientCreditsException extends BusinessException {
151
+ constructor(required: number, available: number) {
152
+ super(
153
+ 'INSUFFICIENT_CREDITS',
154
+ `Required ${required} credits but only ${available} available`,
155
+ HttpStatus.PAYMENT_REQUIRED,
156
+ );
157
+ }
158
+ }
159
+
160
+ export class DuplicateEmailException extends BusinessException {
161
+ constructor(email: string) {
162
+ super('DUPLICATE_EMAIL', `Email ${email} is already registered`, HttpStatus.CONFLICT);
163
+ }
164
+ }
165
+ ```
166
+
167
+ ## Domain Exception Filter
168
+
169
+ ```typescript
170
+ // filters/business-exception.filter.ts
171
+ import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
172
+ import { Response } from 'express';
173
+ import { BusinessException } from '../exceptions/business.exception';
174
+
175
+ @Catch(BusinessException)
176
+ export class BusinessExceptionFilter implements ExceptionFilter {
177
+ catch(exception: BusinessException, host: ArgumentsHost) {
178
+ const ctx = host.switchToHttp();
179
+ const response = ctx.getResponse<Response>();
180
+ const status = exception.getStatus();
181
+
182
+ response.status(status).json({
183
+ type: `https://api.example.com/errors/${exception.code}`,
184
+ title: exception.code.replace(/_/g, ' ').toLowerCase(),
185
+ status,
186
+ detail: exception.message,
187
+ });
188
+ }
189
+ }
190
+ ```
191
+
192
+ ## Database Exception Filter
193
+
194
+ ```typescript
195
+ // filters/prisma-exception.filter.ts
196
+ import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
197
+ import { Prisma } from '@prisma/client';
198
+ import { Response } from 'express';
199
+
200
+ @Catch(Prisma.PrismaClientKnownRequestError)
201
+ export class PrismaExceptionFilter implements ExceptionFilter {
202
+ catch(exception: Prisma.PrismaClientKnownRequestError, host: ArgumentsHost) {
203
+ const ctx = host.switchToHttp();
204
+ const response = ctx.getResponse<Response>();
205
+
206
+ switch (exception.code) {
207
+ case 'P2002': // Unique constraint violation
208
+ const field = (exception.meta?.target as string[])?.[0] || 'field';
209
+ response.status(HttpStatus.CONFLICT).json({
210
+ statusCode: HttpStatus.CONFLICT,
211
+ message: `Duplicate value for ${field}`,
212
+ error: 'Conflict',
213
+ });
214
+ break;
215
+
216
+ case 'P2025': // Record not found
217
+ response.status(HttpStatus.NOT_FOUND).json({
218
+ statusCode: HttpStatus.NOT_FOUND,
219
+ message: 'Record not found',
220
+ error: 'Not Found',
221
+ });
222
+ break;
223
+
224
+ case 'P2003': // Foreign key constraint
225
+ response.status(HttpStatus.BAD_REQUEST).json({
226
+ statusCode: HttpStatus.BAD_REQUEST,
227
+ message: 'Related record not found',
228
+ error: 'Bad Request',
229
+ });
230
+ break;
231
+
232
+ default:
233
+ response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
234
+ statusCode: HttpStatus.INTERNAL_SERVER_ERROR,
235
+ message: 'Database error',
236
+ error: 'Internal Server Error',
237
+ });
238
+ }
239
+ }
240
+ }
241
+ ```
242
+
243
+ ## Validation Exception Filter
244
+
245
+ ```typescript
246
+ // filters/validation-exception.filter.ts
247
+ import { ExceptionFilter, Catch, ArgumentsHost, BadRequestException } from '@nestjs/common';
248
+ import { Response } from 'express';
249
+
250
+ @Catch(BadRequestException)
251
+ export class ValidationExceptionFilter implements ExceptionFilter {
252
+ catch(exception: BadRequestException, host: ArgumentsHost) {
253
+ const ctx = host.switchToHttp();
254
+ const response = ctx.getResponse<Response>();
255
+ const exceptionResponse = exception.getResponse() as Record<string, unknown>;
256
+
257
+ // Handle class-validator errors
258
+ if (exceptionResponse.message && Array.isArray(exceptionResponse.message)) {
259
+ response.status(400).json({
260
+ statusCode: 400,
261
+ error: 'Validation Error',
262
+ message: 'Request validation failed',
263
+ details: exceptionResponse.message.map((msg: string) => {
264
+ const [field, ...rest] = msg.split(' ');
265
+ return { field, message: rest.join(' ') };
266
+ }),
267
+ });
268
+ return;
269
+ }
270
+
271
+ response.status(400).json(exceptionResponse);
272
+ }
273
+ }
274
+ ```
275
+
276
+ ## Filter Binding
277
+
278
+ ```typescript
279
+ // Method level
280
+ @Post()
281
+ @UseFilters(HttpExceptionFilter)
282
+ create(@Body() dto: CreateDto) {}
283
+
284
+ // Controller level
285
+ @Controller('users')
286
+ @UseFilters(AllExceptionsFilter)
287
+ export class UsersController {}
288
+
289
+ // Global level (main.ts)
290
+ app.useGlobalFilters(new AllExceptionsFilter());
291
+
292
+ // Global with DI
293
+ @Module({
294
+ providers: [
295
+ {
296
+ provide: APP_FILTER,
297
+ useClass: AllExceptionsFilter,
298
+ },
299
+ ],
300
+ })
301
+ export class AppModule {}
302
+ ```
303
+
304
+ ## Filter Order
305
+
306
+ ```typescript
307
+ // Filters are applied in reverse order (last registered = first executed)
308
+ @UseFilters(
309
+ AllExceptionsFilter, // Fallback (executed last)
310
+ HttpExceptionFilter, // HTTP exceptions
311
+ BusinessExceptionFilter, // Business exceptions (executed first)
312
+ )
313
+ export class AppController {}
314
+ ```
315
+
316
+ ## WebSocket Exception Filter
317
+
318
+ ```typescript
319
+ // filters/ws-exception.filter.ts
320
+ import { Catch, ArgumentsHost } from '@nestjs/common';
321
+ import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
322
+
323
+ @Catch()
324
+ export class WsExceptionsFilter extends BaseWsExceptionFilter {
325
+ catch(exception: unknown, host: ArgumentsHost) {
326
+ const client = host.switchToWs().getClient();
327
+
328
+ const error =
329
+ exception instanceof WsException
330
+ ? exception.getError()
331
+ : { message: 'Internal error' };
332
+
333
+ client.emit('error', {
334
+ event: 'error',
335
+ data: error,
336
+ });
337
+ }
338
+ }
339
+ ```
340
+
341
+ ## Anti-patterns
342
+
343
+ ```typescript
344
+ // BAD: Swallowing errors without logging
345
+ @Catch()
346
+ export class SilentFilter implements ExceptionFilter {
347
+ catch(exception: unknown, host: ArgumentsHost) {
348
+ const response = host.switchToHttp().getResponse();
349
+ response.status(500).json({ error: 'Error' }); // No logging!
350
+ }
351
+ }
352
+
353
+ // GOOD: Always log errors
354
+ this.logger.error(exception);
355
+
356
+ // BAD: Exposing internal details
357
+ catch(exception: Error, host: ArgumentsHost) {
358
+ response.json({
359
+ stack: exception.stack, // Security risk!
360
+ query: request.query, // Leaking data!
361
+ });
362
+ }
363
+
364
+ // GOOD: Sanitize response
365
+ response.json({
366
+ statusCode: 500,
367
+ message: 'Internal server error',
368
+ });
369
+
370
+ // BAD: Not handling specific exceptions
371
+ @Catch()
372
+ export class GenericFilter {} // Catches everything the same way
373
+
374
+ // GOOD: Layer filters by specificity
375
+ @UseFilters(AllExceptionsFilter, HttpExceptionFilter, BusinessExceptionFilter)
376
+ ```
@@ -0,0 +1,317 @@
1
+ ---
2
+ paths:
3
+ - "src/**/*.interceptor.ts"
4
+ - "src/**/interceptors/**/*.ts"
5
+ - "src/**/*.controller.ts"
6
+ - "src/**/*.module.ts"
7
+ ---
8
+
9
+ # NestJS Interceptors
10
+
11
+ ## Interceptor Basics
12
+
13
+ Interceptors can:
14
+ - Transform response data
15
+ - Transform exceptions
16
+ - Extend/override function behavior
17
+ - Implement caching, logging, timeout
18
+
19
+ ```typescript
20
+ import {
21
+ Injectable,
22
+ NestInterceptor,
23
+ ExecutionContext,
24
+ CallHandler,
25
+ } from '@nestjs/common';
26
+ import { Observable } from 'rxjs';
27
+ import { map, tap } from 'rxjs/operators';
28
+
29
+ @Injectable()
30
+ export class LoggingInterceptor implements NestInterceptor {
31
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
32
+ const request = context.switchToHttp().getRequest();
33
+ const { method, url } = request;
34
+ const now = Date.now();
35
+
36
+ console.log(`[${method}] ${url} - Started`);
37
+
38
+ return next.handle().pipe(
39
+ tap(() => {
40
+ console.log(`[${method}] ${url} - ${Date.now() - now}ms`);
41
+ }),
42
+ );
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Response Transform Interceptor
48
+
49
+ ```typescript
50
+ // Wrap all responses in standard format
51
+ export interface ApiResponse<T> {
52
+ data: T;
53
+ meta: {
54
+ timestamp: string;
55
+ requestId: string;
56
+ };
57
+ }
58
+
59
+ @Injectable()
60
+ export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
61
+ intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
62
+ const request = context.switchToHttp().getRequest();
63
+
64
+ return next.handle().pipe(
65
+ map((data) => ({
66
+ data,
67
+ meta: {
68
+ timestamp: new Date().toISOString(),
69
+ requestId: request.id,
70
+ },
71
+ })),
72
+ );
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## Timeout Interceptor
78
+
79
+ ```typescript
80
+ import { Injectable, RequestTimeoutException } from '@nestjs/common';
81
+ import { Observable, throwError, TimeoutError } from 'rxjs';
82
+ import { catchError, timeout } from 'rxjs/operators';
83
+
84
+ @Injectable()
85
+ export class TimeoutInterceptor implements NestInterceptor {
86
+ constructor(private readonly timeoutMs: number = 30000) {}
87
+
88
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
89
+ return next.handle().pipe(
90
+ timeout(this.timeoutMs),
91
+ catchError((err) => {
92
+ if (err instanceof TimeoutError) {
93
+ return throwError(() => new RequestTimeoutException('Request timeout'));
94
+ }
95
+ return throwError(() => err);
96
+ }),
97
+ );
98
+ }
99
+ }
100
+ ```
101
+
102
+ ## Caching Interceptor
103
+
104
+ ```typescript
105
+ import { CACHE_MANAGER } from '@nestjs/cache-manager';
106
+ import { Cache } from 'cache-manager';
107
+
108
+ @Injectable()
109
+ export class HttpCacheInterceptor implements NestInterceptor {
110
+ constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}
111
+
112
+ async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
113
+ const request = context.switchToHttp().getRequest();
114
+
115
+ // Only cache GET requests
116
+ if (request.method !== 'GET') {
117
+ return next.handle();
118
+ }
119
+
120
+ const cacheKey = `http:${request.url}`;
121
+ const cachedResponse = await this.cacheManager.get(cacheKey);
122
+
123
+ if (cachedResponse) {
124
+ return of(cachedResponse);
125
+ }
126
+
127
+ return next.handle().pipe(
128
+ tap(async (response) => {
129
+ await this.cacheManager.set(cacheKey, response, 60000); // 60s TTL
130
+ }),
131
+ );
132
+ }
133
+ }
134
+ ```
135
+
136
+ ## Error Mapping Interceptor
137
+
138
+ ```typescript
139
+ @Injectable()
140
+ export class ErrorMappingInterceptor implements NestInterceptor {
141
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
142
+ return next.handle().pipe(
143
+ catchError((error) => {
144
+ // Map domain errors to HTTP errors
145
+ if (error instanceof EntityNotFoundError) {
146
+ return throwError(() => new NotFoundException(error.message));
147
+ }
148
+ if (error instanceof ValidationError) {
149
+ return throwError(() => new BadRequestException(error.errors));
150
+ }
151
+ if (error instanceof UnauthorizedError) {
152
+ return throwError(() => new UnauthorizedException(error.message));
153
+ }
154
+
155
+ // Re-throw unknown errors
156
+ return throwError(() => error);
157
+ }),
158
+ );
159
+ }
160
+ }
161
+ ```
162
+
163
+ ## Serialization Interceptor
164
+
165
+ ```typescript
166
+ import { ClassSerializerInterceptor, PlainLiteralObject } from '@nestjs/common';
167
+ import { ClassTransformOptions, plainToClass } from 'class-transformer';
168
+
169
+ @Injectable()
170
+ export class CustomSerializerInterceptor extends ClassSerializerInterceptor {
171
+ transformToPlain(
172
+ data: object,
173
+ options: ClassTransformOptions,
174
+ ): PlainLiteralObject | PlainLiteralObject[] {
175
+ // Add custom serialization logic
176
+ return super.transformToPlain(data, {
177
+ ...options,
178
+ excludeExtraneousValues: true, // Only @Expose() fields
179
+ });
180
+ }
181
+ }
182
+
183
+ // DTO with explicit exposure
184
+ export class UserResponseDto {
185
+ @Expose()
186
+ id: string;
187
+
188
+ @Expose()
189
+ email: string;
190
+
191
+ @Expose()
192
+ name: string;
193
+
194
+ @Exclude()
195
+ password: string; // Never exposed
196
+
197
+ @Expose()
198
+ @Transform(({ value }) => value.toISOString())
199
+ createdAt: Date;
200
+ }
201
+ ```
202
+
203
+ ## Audit Log Interceptor
204
+
205
+ ```typescript
206
+ @Injectable()
207
+ export class AuditLogInterceptor implements NestInterceptor {
208
+ constructor(private readonly auditService: AuditService) {}
209
+
210
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
211
+ const request = context.switchToHttp().getRequest();
212
+ const { method, url, user, body } = request;
213
+
214
+ // Only audit mutating operations
215
+ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
216
+ return next.handle().pipe(
217
+ tap({
218
+ next: (response) => {
219
+ this.auditService.log({
220
+ userId: user?.id,
221
+ action: method,
222
+ resource: url,
223
+ input: this.sanitize(body),
224
+ output: this.sanitize(response),
225
+ timestamp: new Date(),
226
+ });
227
+ },
228
+ error: (error) => {
229
+ this.auditService.log({
230
+ userId: user?.id,
231
+ action: method,
232
+ resource: url,
233
+ input: this.sanitize(body),
234
+ error: error.message,
235
+ timestamp: new Date(),
236
+ });
237
+ },
238
+ }),
239
+ );
240
+ }
241
+
242
+ return next.handle();
243
+ }
244
+
245
+ private sanitize(data: Record<string, unknown> | null): Record<string, unknown> | null {
246
+ if (!data) return data;
247
+ const { password, token, secret, ...safe } = data;
248
+ return safe;
249
+ }
250
+ }
251
+ ```
252
+
253
+ ## Applying Interceptors
254
+
255
+ ```typescript
256
+ // Global (main.ts)
257
+ app.useGlobalInterceptors(new LoggingInterceptor());
258
+
259
+ // Global with DI (module)
260
+ @Module({
261
+ providers: [
262
+ {
263
+ provide: APP_INTERCEPTOR,
264
+ useClass: TransformInterceptor,
265
+ },
266
+ ],
267
+ })
268
+ export class AppModule {}
269
+
270
+ // Controller level
271
+ @UseInterceptors(LoggingInterceptor)
272
+ @Controller('users')
273
+ export class UsersController {}
274
+
275
+ // Method level
276
+ @UseInterceptors(CacheInterceptor)
277
+ @Get(':id')
278
+ findOne(@Param('id') id: string) {}
279
+
280
+ // Multiple interceptors (order matters: first to last)
281
+ @UseInterceptors(LoggingInterceptor, TransformInterceptor)
282
+ @Controller('users')
283
+ export class UsersController {}
284
+ ```
285
+
286
+ ## Custom Decorator for Interceptors
287
+
288
+ ```typescript
289
+ // Skip certain interceptors conditionally
290
+ export const SKIP_TRANSFORM = 'skipTransform';
291
+ export const SkipTransform = () => SetMetadata(SKIP_TRANSFORM, true);
292
+
293
+ @Injectable()
294
+ export class TransformInterceptor implements NestInterceptor {
295
+ constructor(private reflector: Reflector) {}
296
+
297
+ intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
298
+ const skipTransform = this.reflector.get<boolean>(
299
+ SKIP_TRANSFORM,
300
+ context.getHandler(),
301
+ );
302
+
303
+ if (skipTransform) {
304
+ return next.handle();
305
+ }
306
+
307
+ return next.handle().pipe(map((data) => ({ data })));
308
+ }
309
+ }
310
+
311
+ // Usage
312
+ @SkipTransform()
313
+ @Get('raw')
314
+ getRawData() {
315
+ return { raw: true };
316
+ }
317
+ ```