@malamute/ai-rules 1.0.0 → 1.2.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.
- package/README.md +270 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/.claude/rules/conventions/documentation.md +324 -0
- package/configs/_shared/.claude/rules/conventions/git.md +265 -0
- package/configs/_shared/.claude/rules/{performance.md → conventions/performance.md} +1 -1
- package/configs/_shared/.claude/rules/conventions/principles.md +334 -0
- package/configs/_shared/.claude/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/.claude/rules/devops/docker.md +275 -0
- package/configs/_shared/.claude/rules/devops/nx.md +194 -0
- package/configs/_shared/.claude/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/.claude/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/.claude/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/.claude/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/.claude/rules/lang/python/async.md +337 -0
- package/configs/_shared/.claude/rules/lang/python/celery.md +476 -0
- package/configs/_shared/.claude/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/.claude/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/.claude/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/.claude/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/.claude/rules/lang/python/python.md +172 -0
- package/configs/_shared/.claude/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/.claude/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/.claude/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/.claude/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/.claude/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/.claude/rules/quality/error-handling.md +48 -0
- package/configs/_shared/.claude/rules/quality/logging.md +45 -0
- package/configs/_shared/.claude/rules/quality/observability.md +240 -0
- package/configs/_shared/.claude/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/.claude/rules/security/secrets-management.md +222 -0
- package/configs/_shared/.claude/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/.claude/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/.claude/skills/dev/api-endpoint/SKILL.md +126 -0
- package/configs/_shared/.claude/{commands/generate-tests.md → skills/dev/generate-tests/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/fix-issue.md → skills/git/fix-issue/SKILL.md} +6 -0
- package/configs/_shared/.claude/{commands/review-pr.md → skills/git/review-pr/SKILL.md} +6 -0
- package/configs/_shared/.claude/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/.claude/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/.claude/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/.claude/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/.claude/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/angular/.claude/rules/{components.md → core/components.md} +69 -15
- package/configs/angular/.claude/rules/core/resource.md +285 -0
- package/configs/angular/.claude/rules/core/signals.md +323 -0
- package/configs/angular/.claude/rules/http.md +338 -0
- package/configs/angular/.claude/rules/routing.md +291 -0
- package/configs/angular/.claude/rules/ssr.md +312 -0
- package/configs/angular/.claude/rules/state/signal-store.md +408 -0
- package/configs/angular/.claude/rules/{state.md → state/state.md} +2 -2
- package/configs/angular/.claude/rules/testing.md +7 -7
- package/configs/angular/.claude/rules/ui/aria.md +422 -0
- package/configs/angular/.claude/rules/ui/forms.md +424 -0
- package/configs/angular/.claude/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/.claude/settings.json +1 -0
- package/configs/angular/.claude/skills/ngrx-slice/SKILL.md +362 -0
- package/configs/angular/.claude/skills/signal-store/SKILL.md +445 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/dotnet/.claude/rules/background-services.md +552 -0
- package/configs/dotnet/.claude/rules/configuration.md +426 -0
- package/configs/dotnet/.claude/rules/ddd.md +447 -0
- package/configs/dotnet/.claude/rules/dependency-injection.md +343 -0
- package/configs/dotnet/.claude/rules/mediatr.md +320 -0
- package/configs/dotnet/.claude/rules/middleware.md +489 -0
- package/configs/dotnet/.claude/rules/result-pattern.md +363 -0
- package/configs/dotnet/.claude/rules/validation.md +388 -0
- package/configs/dotnet/.claude/settings.json +21 -3
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/fastapi/.claude/rules/background-tasks.md +254 -0
- package/configs/fastapi/.claude/rules/dependencies.md +170 -0
- package/configs/{python → fastapi}/.claude/rules/fastapi.md +61 -1
- package/configs/fastapi/.claude/rules/lifespan.md +274 -0
- package/configs/fastapi/.claude/rules/middleware.md +229 -0
- package/configs/fastapi/.claude/rules/pydantic.md +433 -0
- package/configs/fastapi/.claude/rules/responses.md +251 -0
- package/configs/fastapi/.claude/rules/routers.md +202 -0
- package/configs/fastapi/.claude/rules/security.md +222 -0
- package/configs/fastapi/.claude/rules/testing.md +251 -0
- package/configs/fastapi/.claude/rules/websockets.md +298 -0
- package/configs/fastapi/.claude/settings.json +33 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/flask/.claude/rules/blueprints.md +208 -0
- package/configs/flask/.claude/rules/cli.md +285 -0
- package/configs/flask/.claude/rules/configuration.md +281 -0
- package/configs/flask/.claude/rules/context.md +238 -0
- package/configs/flask/.claude/rules/error-handlers.md +278 -0
- package/configs/flask/.claude/rules/extensions.md +278 -0
- package/configs/flask/.claude/rules/flask.md +171 -0
- package/configs/flask/.claude/rules/marshmallow.md +206 -0
- package/configs/flask/.claude/rules/security.md +267 -0
- package/configs/flask/.claude/rules/testing.md +284 -0
- package/configs/flask/.claude/settings.json +33 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/nestjs/.claude/rules/common-patterns.md +300 -0
- package/configs/nestjs/.claude/rules/filters.md +376 -0
- package/configs/nestjs/.claude/rules/interceptors.md +317 -0
- package/configs/nestjs/.claude/rules/middleware.md +321 -0
- package/configs/nestjs/.claude/rules/modules.md +26 -0
- package/configs/nestjs/.claude/rules/pipes.md +351 -0
- package/configs/nestjs/.claude/rules/websockets.md +451 -0
- package/configs/nestjs/.claude/settings.json +16 -2
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nextjs/.claude/rules/api-routes.md +358 -0
- package/configs/nextjs/.claude/rules/authentication.md +355 -0
- package/configs/nextjs/.claude/rules/components.md +52 -0
- package/configs/nextjs/.claude/rules/data-fetching.md +249 -0
- package/configs/nextjs/.claude/rules/database.md +400 -0
- package/configs/nextjs/.claude/rules/middleware.md +303 -0
- package/configs/nextjs/.claude/rules/routing.md +324 -0
- package/configs/nextjs/.claude/rules/seo.md +350 -0
- package/configs/nextjs/.claude/rules/server-actions.md +353 -0
- package/configs/nextjs/.claude/rules/state/zustand.md +6 -6
- package/configs/nextjs/.claude/settings.json +5 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/package.json +23 -9
- package/src/cli.js +220 -0
- package/src/config.js +29 -0
- package/src/index.js +13 -0
- package/src/installer.js +361 -0
- package/src/merge.js +116 -0
- package/src/tech-config.json +29 -0
- package/src/utils.js +96 -0
- package/configs/python/.claude/rules/flask.md +0 -332
- package/configs/python/.claude/settings.json +0 -18
- package/configs/python/CLAUDE.md +0 -273
- package/src/install.js +0 -315
- /package/configs/_shared/.claude/rules/{accessibility.md → domain/frontend/accessibility.md} +0 -0
- /package/configs/_shared/.claude/rules/{security.md → security/security.md} +0 -0
- /package/configs/_shared/.claude/skills/{debug → dev/debug}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{learning → dev/learning}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{spec → dev/spec}/SKILL.md +0 -0
- /package/configs/_shared/.claude/skills/{review → git/review}/SKILL.md +0 -0
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.middleware.ts"
|
|
4
|
+
- "**/middleware/**/*.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# NestJS Middleware
|
|
8
|
+
|
|
9
|
+
## Functional Middleware
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// middleware/logger.middleware.ts
|
|
13
|
+
import { Request, Response, NextFunction } from 'express';
|
|
14
|
+
|
|
15
|
+
export function loggerMiddleware(req: Request, res: Response, next: NextFunction) {
|
|
16
|
+
const start = Date.now();
|
|
17
|
+
|
|
18
|
+
res.on('finish', () => {
|
|
19
|
+
const duration = Date.now() - start;
|
|
20
|
+
console.log(`${req.method} ${req.url} ${res.statusCode} - ${duration}ms`);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
next();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Apply in module
|
|
27
|
+
export class AppModule implements NestModule {
|
|
28
|
+
configure(consumer: MiddlewareConsumer) {
|
|
29
|
+
consumer.apply(loggerMiddleware).forRoutes('*');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Class Middleware
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// middleware/auth.middleware.ts
|
|
38
|
+
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
|
|
39
|
+
import { Request, Response, NextFunction } from 'express';
|
|
40
|
+
import { JwtService } from '@nestjs/jwt';
|
|
41
|
+
|
|
42
|
+
@Injectable()
|
|
43
|
+
export class AuthMiddleware implements NestMiddleware {
|
|
44
|
+
constructor(private readonly jwtService: JwtService) {}
|
|
45
|
+
|
|
46
|
+
async use(req: Request, res: Response, next: NextFunction) {
|
|
47
|
+
const authHeader = req.headers.authorization;
|
|
48
|
+
|
|
49
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
50
|
+
throw new UnauthorizedException('Missing token');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const token = authHeader.split(' ')[1];
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const payload = await this.jwtService.verifyAsync(token);
|
|
57
|
+
req['user'] = payload;
|
|
58
|
+
next();
|
|
59
|
+
} catch {
|
|
60
|
+
throw new UnauthorizedException('Invalid token');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Correlation ID Middleware
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// middleware/correlation-id.middleware.ts
|
|
70
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
71
|
+
import { Request, Response, NextFunction } from 'express';
|
|
72
|
+
import { randomUUID } from 'crypto';
|
|
73
|
+
|
|
74
|
+
@Injectable()
|
|
75
|
+
export class CorrelationIdMiddleware implements NestMiddleware {
|
|
76
|
+
use(req: Request, res: Response, next: NextFunction) {
|
|
77
|
+
const correlationId = req.headers['x-correlation-id'] as string || randomUUID();
|
|
78
|
+
|
|
79
|
+
req['correlationId'] = correlationId;
|
|
80
|
+
res.setHeader('x-correlation-id', correlationId);
|
|
81
|
+
|
|
82
|
+
next();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Request Validation Middleware
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// middleware/content-type.middleware.ts
|
|
91
|
+
import { Injectable, NestMiddleware, UnsupportedMediaTypeException } from '@nestjs/common';
|
|
92
|
+
import { Request, Response, NextFunction } from 'express';
|
|
93
|
+
|
|
94
|
+
@Injectable()
|
|
95
|
+
export class JsonContentTypeMiddleware implements NestMiddleware {
|
|
96
|
+
use(req: Request, res: Response, next: NextFunction) {
|
|
97
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
98
|
+
const contentType = req.headers['content-type'];
|
|
99
|
+
|
|
100
|
+
if (!contentType?.includes('application/json')) {
|
|
101
|
+
throw new UnsupportedMediaTypeException('Content-Type must be application/json');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
next();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Rate Limiting Middleware
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// middleware/rate-limit.middleware.ts
|
|
114
|
+
import { Injectable, NestMiddleware, HttpException, HttpStatus } from '@nestjs/common';
|
|
115
|
+
import { Request, Response, NextFunction } from 'express';
|
|
116
|
+
import { Redis } from 'ioredis';
|
|
117
|
+
|
|
118
|
+
@Injectable()
|
|
119
|
+
export class RateLimitMiddleware implements NestMiddleware {
|
|
120
|
+
private readonly windowMs = 60 * 1000; // 1 minute
|
|
121
|
+
private readonly maxRequests = 100;
|
|
122
|
+
|
|
123
|
+
constructor(private readonly redis: Redis) {}
|
|
124
|
+
|
|
125
|
+
async use(req: Request, res: Response, next: NextFunction) {
|
|
126
|
+
const key = `rate-limit:${req.ip}`;
|
|
127
|
+
const current = await this.redis.incr(key);
|
|
128
|
+
|
|
129
|
+
if (current === 1) {
|
|
130
|
+
await this.redis.pexpire(key, this.windowMs);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const remaining = Math.max(0, this.maxRequests - current);
|
|
134
|
+
const ttl = await this.redis.pttl(key);
|
|
135
|
+
|
|
136
|
+
res.setHeader('X-RateLimit-Limit', this.maxRequests);
|
|
137
|
+
res.setHeader('X-RateLimit-Remaining', remaining);
|
|
138
|
+
res.setHeader('X-RateLimit-Reset', Math.ceil(Date.now() / 1000 + ttl / 1000));
|
|
139
|
+
|
|
140
|
+
if (current > this.maxRequests) {
|
|
141
|
+
throw new HttpException('Too Many Requests', HttpStatus.TOO_MANY_REQUESTS);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
next();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Request Sanitization Middleware
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
// middleware/sanitize.middleware.ts
|
|
153
|
+
import { Injectable, NestMiddleware } from '@nestjs/common';
|
|
154
|
+
import { Request, Response, NextFunction } from 'express';
|
|
155
|
+
import * as sanitizeHtml from 'sanitize-html';
|
|
156
|
+
|
|
157
|
+
@Injectable()
|
|
158
|
+
export class SanitizeMiddleware implements NestMiddleware {
|
|
159
|
+
use(req: Request, res: Response, next: NextFunction) {
|
|
160
|
+
if (req.body) {
|
|
161
|
+
req.body = this.sanitizeObject(req.body);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
next();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private sanitizeObject(obj: Record<string, unknown>): Record<string, unknown> {
|
|
168
|
+
const sanitized: Record<string, unknown> = {};
|
|
169
|
+
|
|
170
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
171
|
+
if (typeof value === 'string') {
|
|
172
|
+
sanitized[key] = sanitizeHtml(value, {
|
|
173
|
+
allowedTags: [],
|
|
174
|
+
allowedAttributes: {},
|
|
175
|
+
});
|
|
176
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
177
|
+
sanitized[key] = this.sanitizeObject(value as Record<string, unknown>);
|
|
178
|
+
} else {
|
|
179
|
+
sanitized[key] = value;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return sanitized;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Applying Middleware
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// app.module.ts
|
|
192
|
+
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
|
|
193
|
+
|
|
194
|
+
@Module({})
|
|
195
|
+
export class AppModule implements NestModule {
|
|
196
|
+
configure(consumer: MiddlewareConsumer) {
|
|
197
|
+
// Apply to all routes
|
|
198
|
+
consumer.apply(CorrelationIdMiddleware).forRoutes('*');
|
|
199
|
+
|
|
200
|
+
// Apply to specific path
|
|
201
|
+
consumer.apply(AuthMiddleware).forRoutes('api/protected');
|
|
202
|
+
|
|
203
|
+
// Apply to specific controller
|
|
204
|
+
consumer.apply(LoggerMiddleware).forRoutes(UsersController);
|
|
205
|
+
|
|
206
|
+
// Apply with method filter
|
|
207
|
+
consumer
|
|
208
|
+
.apply(JsonContentTypeMiddleware)
|
|
209
|
+
.forRoutes({ path: '*', method: RequestMethod.POST });
|
|
210
|
+
|
|
211
|
+
// Exclude routes
|
|
212
|
+
consumer
|
|
213
|
+
.apply(AuthMiddleware)
|
|
214
|
+
.exclude(
|
|
215
|
+
{ path: 'auth/login', method: RequestMethod.POST },
|
|
216
|
+
{ path: 'auth/register', method: RequestMethod.POST },
|
|
217
|
+
{ path: 'health', method: RequestMethod.GET },
|
|
218
|
+
)
|
|
219
|
+
.forRoutes('*');
|
|
220
|
+
|
|
221
|
+
// Chain multiple middleware
|
|
222
|
+
consumer
|
|
223
|
+
.apply(CorrelationIdMiddleware, LoggerMiddleware, AuthMiddleware)
|
|
224
|
+
.forRoutes('api');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Global Middleware (Express)
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// main.ts
|
|
233
|
+
import * as helmet from 'helmet';
|
|
234
|
+
import * as compression from 'compression';
|
|
235
|
+
import * as cookieParser from 'cookie-parser';
|
|
236
|
+
|
|
237
|
+
async function bootstrap() {
|
|
238
|
+
const app = await NestFactory.create(AppModule);
|
|
239
|
+
|
|
240
|
+
// Security headers
|
|
241
|
+
app.use(helmet());
|
|
242
|
+
|
|
243
|
+
// Compression
|
|
244
|
+
app.use(compression());
|
|
245
|
+
|
|
246
|
+
// Cookie parsing
|
|
247
|
+
app.use(cookieParser());
|
|
248
|
+
|
|
249
|
+
// CORS
|
|
250
|
+
app.enableCors({
|
|
251
|
+
origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
|
|
252
|
+
credentials: true,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Body parsing limits
|
|
256
|
+
app.use(express.json({ limit: '10mb' }));
|
|
257
|
+
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
258
|
+
|
|
259
|
+
await app.listen(3000);
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Middleware vs Guards vs Interceptors
|
|
264
|
+
|
|
265
|
+
| Feature | Middleware | Guards | Interceptors |
|
|
266
|
+
|---------|------------|--------|--------------|
|
|
267
|
+
| Execution Order | First | After middleware | After guards |
|
|
268
|
+
| Access to ExecutionContext | No | Yes | Yes |
|
|
269
|
+
| Can transform response | No | No | Yes |
|
|
270
|
+
| DI support | Class only | Yes | Yes |
|
|
271
|
+
| Use case | Request processing | Authorization | Transform/logging |
|
|
272
|
+
|
|
273
|
+
## Anti-patterns
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
// BAD: Heavy operations in middleware
|
|
277
|
+
@Injectable()
|
|
278
|
+
export class HeavyMiddleware implements NestMiddleware {
|
|
279
|
+
async use(req: Request, res: Response, next: NextFunction) {
|
|
280
|
+
await this.db.query('SELECT * FROM logs'); // Blocks every request!
|
|
281
|
+
next();
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// GOOD: Keep middleware lightweight
|
|
286
|
+
|
|
287
|
+
// BAD: Not calling next()
|
|
288
|
+
use(req: Request, res: Response, next: NextFunction) {
|
|
289
|
+
if (someCondition) {
|
|
290
|
+
return; // Request hangs!
|
|
291
|
+
}
|
|
292
|
+
next();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// GOOD: Always call next() or send response
|
|
296
|
+
use(req: Request, res: Response, next: NextFunction) {
|
|
297
|
+
if (someCondition) {
|
|
298
|
+
res.status(400).json({ error: 'Bad request' });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
next();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// BAD: Modifying response after next()
|
|
305
|
+
async use(req: Request, res: Response, next: NextFunction) {
|
|
306
|
+
next();
|
|
307
|
+
res.setHeader('X-Custom', 'value'); // May not work!
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// GOOD: Modify before next() or use interceptor
|
|
311
|
+
use(req: Request, res: Response, next: NextFunction) {
|
|
312
|
+
res.setHeader('X-Custom', 'value');
|
|
313
|
+
next();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// BAD: Using middleware for auth when guards exist
|
|
317
|
+
// Middleware can't access ExecutionContext
|
|
318
|
+
|
|
319
|
+
// GOOD: Use guards for authorization
|
|
320
|
+
@UseGuards(AuthGuard)
|
|
321
|
+
```
|
|
@@ -3,10 +3,36 @@ paths:
|
|
|
3
3
|
- "src/**/*.module.ts"
|
|
4
4
|
- "src/**/*.controller.ts"
|
|
5
5
|
- "src/**/*.service.ts"
|
|
6
|
+
- "src/main.ts"
|
|
6
7
|
---
|
|
7
8
|
|
|
8
9
|
# NestJS Module Architecture
|
|
9
10
|
|
|
11
|
+
## Global Configuration (main.ts)
|
|
12
|
+
|
|
13
|
+
### ValidationPipe Setup
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
17
|
+
|
|
18
|
+
async function bootstrap() {
|
|
19
|
+
const app = await NestFactory.create(AppModule);
|
|
20
|
+
|
|
21
|
+
app.useGlobalPipes(
|
|
22
|
+
new ValidationPipe({
|
|
23
|
+
whitelist: true, // Strip non-whitelisted properties
|
|
24
|
+
forbidNonWhitelisted: true, // Throw error on extra properties
|
|
25
|
+
transform: true, // Auto-transform payloads to DTO types
|
|
26
|
+
transformOptions: {
|
|
27
|
+
enableImplicitConversion: true,
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
await app.listen(3000);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
10
36
|
## Module Design
|
|
11
37
|
|
|
12
38
|
### Single Responsibility
|