@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.
- package/README.md +272 -121
- package/bin/cli.js +5 -2
- package/configs/_shared/CLAUDE.md +52 -149
- package/configs/_shared/rules/conventions/documentation.md +324 -0
- package/configs/_shared/rules/conventions/git.md +265 -0
- package/configs/_shared/rules/conventions/npm.md +80 -0
- package/configs/_shared/{.claude/rules → rules/conventions}/performance.md +1 -1
- package/configs/_shared/rules/conventions/principles.md +334 -0
- package/configs/_shared/rules/devops/ci-cd.md +262 -0
- package/configs/_shared/rules/devops/docker.md +275 -0
- package/configs/_shared/rules/devops/nx.md +194 -0
- package/configs/_shared/rules/domain/backend/api-design.md +203 -0
- package/configs/_shared/rules/lang/csharp/async.md +220 -0
- package/configs/_shared/rules/lang/csharp/csharp.md +314 -0
- package/configs/_shared/rules/lang/csharp/linq.md +210 -0
- package/configs/_shared/rules/lang/python/async.md +337 -0
- package/configs/_shared/rules/lang/python/celery.md +476 -0
- package/configs/_shared/rules/lang/python/config.md +339 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/database/sqlalchemy.md +6 -1
- package/configs/_shared/rules/lang/python/deployment.md +523 -0
- package/configs/_shared/rules/lang/python/error-handling.md +330 -0
- package/configs/_shared/rules/lang/python/migrations.md +421 -0
- package/configs/_shared/rules/lang/python/python.md +172 -0
- package/configs/_shared/rules/lang/python/repository.md +383 -0
- package/configs/{python/.claude/rules → _shared/rules/lang/python}/testing.md +2 -69
- package/configs/_shared/rules/lang/typescript/async.md +447 -0
- package/configs/_shared/rules/lang/typescript/generics.md +356 -0
- package/configs/_shared/rules/lang/typescript/typescript.md +212 -0
- package/configs/_shared/rules/quality/error-handling.md +48 -0
- package/configs/_shared/rules/quality/logging.md +45 -0
- package/configs/_shared/rules/quality/observability.md +240 -0
- package/configs/_shared/rules/quality/testing-patterns.md +65 -0
- package/configs/_shared/rules/security/secrets-management.md +222 -0
- package/configs/_shared/skills/analysis/explore/SKILL.md +257 -0
- package/configs/_shared/skills/analysis/security-audit/SKILL.md +184 -0
- package/configs/_shared/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/skills/infra/deploy/SKILL.md +139 -0
- package/configs/_shared/skills/infra/docker/SKILL.md +95 -0
- package/configs/_shared/skills/infra/migration/SKILL.md +158 -0
- package/configs/_shared/skills/nx/nx-affected/SKILL.md +72 -0
- package/configs/_shared/skills/nx/nx-lib/SKILL.md +375 -0
- package/configs/angular/CLAUDE.md +24 -216
- package/configs/angular/{.claude/rules → rules/core}/components.md +69 -15
- package/configs/angular/rules/core/resource.md +285 -0
- package/configs/angular/rules/core/signals.md +323 -0
- package/configs/angular/rules/http.md +338 -0
- package/configs/angular/rules/routing.md +291 -0
- package/configs/angular/rules/ssr.md +312 -0
- package/configs/angular/rules/state/signal-store.md +408 -0
- package/configs/angular/{.claude/rules → rules/state}/state.md +2 -2
- package/configs/angular/{.claude/rules → rules}/testing.md +7 -7
- package/configs/angular/rules/ui/aria.md +422 -0
- package/configs/angular/rules/ui/forms.md +424 -0
- package/configs/angular/rules/ui/pipes-directives.md +335 -0
- package/configs/angular/{.claude/settings.json → settings.json} +3 -0
- package/configs/dotnet/CLAUDE.md +53 -286
- package/configs/dotnet/rules/background-services.md +552 -0
- package/configs/dotnet/rules/configuration.md +426 -0
- package/configs/dotnet/rules/ddd.md +447 -0
- package/configs/dotnet/rules/dependency-injection.md +343 -0
- package/configs/dotnet/rules/mediatr.md +320 -0
- package/configs/dotnet/rules/middleware.md +489 -0
- package/configs/dotnet/rules/result-pattern.md +363 -0
- package/configs/dotnet/rules/validation.md +388 -0
- package/configs/dotnet/settings.json +29 -0
- package/configs/fastapi/CLAUDE.md +144 -0
- package/configs/fastapi/rules/background-tasks.md +254 -0
- package/configs/fastapi/rules/dependencies.md +170 -0
- package/configs/{python/.claude → fastapi}/rules/fastapi.md +61 -1
- package/configs/fastapi/rules/lifespan.md +274 -0
- package/configs/fastapi/rules/middleware.md +229 -0
- package/configs/fastapi/rules/pydantic.md +433 -0
- package/configs/fastapi/rules/responses.md +251 -0
- package/configs/fastapi/rules/routers.md +202 -0
- package/configs/fastapi/rules/security.md +222 -0
- package/configs/fastapi/rules/testing.md +251 -0
- package/configs/fastapi/rules/websockets.md +298 -0
- package/configs/fastapi/settings.json +35 -0
- package/configs/flask/CLAUDE.md +166 -0
- package/configs/flask/rules/blueprints.md +208 -0
- package/configs/flask/rules/cli.md +285 -0
- package/configs/flask/rules/configuration.md +281 -0
- package/configs/flask/rules/context.md +238 -0
- package/configs/flask/rules/error-handlers.md +278 -0
- package/configs/flask/rules/extensions.md +278 -0
- package/configs/flask/rules/flask.md +171 -0
- package/configs/flask/rules/marshmallow.md +206 -0
- package/configs/flask/rules/security.md +267 -0
- package/configs/flask/rules/testing.md +284 -0
- package/configs/flask/settings.json +35 -0
- package/configs/nestjs/CLAUDE.md +57 -215
- package/configs/nestjs/rules/common-patterns.md +300 -0
- package/configs/nestjs/rules/filters.md +376 -0
- package/configs/nestjs/rules/interceptors.md +317 -0
- package/configs/nestjs/rules/middleware.md +321 -0
- package/configs/nestjs/{.claude/rules → rules}/modules.md +26 -0
- package/configs/nestjs/rules/pipes.md +351 -0
- package/configs/nestjs/rules/websockets.md +451 -0
- package/configs/nestjs/settings.json +31 -0
- package/configs/nextjs/CLAUDE.md +69 -331
- package/configs/nextjs/rules/api-routes.md +358 -0
- package/configs/nextjs/rules/authentication.md +355 -0
- package/configs/nextjs/{.claude/rules → rules}/components.md +52 -0
- package/configs/nextjs/rules/data-fetching.md +249 -0
- package/configs/nextjs/rules/database.md +400 -0
- package/configs/nextjs/rules/middleware.md +303 -0
- package/configs/nextjs/rules/routing.md +324 -0
- package/configs/nextjs/rules/seo.md +350 -0
- package/configs/nextjs/rules/server-actions.md +353 -0
- package/configs/nextjs/{.claude/rules → rules}/state/zustand.md +6 -6
- package/configs/nextjs/{.claude/settings.json → settings.json} +7 -0
- package/package.json +24 -9
- package/src/cli.js +218 -0
- package/src/config.js +63 -0
- package/src/index.js +4 -0
- package/src/installer.js +414 -0
- package/src/merge.js +109 -0
- package/src/tech-config.json +45 -0
- package/src/utils.js +88 -0
- package/configs/dotnet/.claude/settings.json +0 -9
- package/configs/nestjs/.claude/settings.json +0 -15
- 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 → rules/domain/frontend}/accessibility.md +0 -0
- /package/configs/_shared/{.claude/rules → rules/security}/security.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/debug/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/learning/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/dev}/spec/SKILL.md +0 -0
- /package/configs/_shared/{.claude/skills → skills/git}/review/SKILL.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/api.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/architecture.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/database/efcore.md +0 -0
- /package/configs/dotnet/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/auth.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/prisma.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/database/typeorm.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/testing.md +0 -0
- /package/configs/nestjs/{.claude/rules → rules}/validation.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/state/redux-toolkit.md +0 -0
- /package/configs/nextjs/{.claude/rules → rules}/testing.md +0 -0
|
@@ -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
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "**/*.pipe.ts"
|
|
4
|
+
- "**/pipes/**/*.ts"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# NestJS Pipes
|
|
8
|
+
|
|
9
|
+
## Built-in Pipes
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import {
|
|
13
|
+
ValidationPipe,
|
|
14
|
+
ParseIntPipe,
|
|
15
|
+
ParseBoolPipe,
|
|
16
|
+
ParseArrayPipe,
|
|
17
|
+
ParseUUIDPipe,
|
|
18
|
+
ParseEnumPipe,
|
|
19
|
+
DefaultValuePipe,
|
|
20
|
+
} from '@nestjs/common';
|
|
21
|
+
|
|
22
|
+
@Controller('users')
|
|
23
|
+
export class UsersController {
|
|
24
|
+
// ParseIntPipe
|
|
25
|
+
@Get(':id')
|
|
26
|
+
findOne(@Param('id', ParseIntPipe) id: number) {
|
|
27
|
+
return this.usersService.findOne(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ParseUUIDPipe with version
|
|
31
|
+
@Get(':uuid')
|
|
32
|
+
findByUuid(@Param('uuid', new ParseUUIDPipe({ version: '4' })) uuid: string) {
|
|
33
|
+
return this.usersService.findByUuid(uuid);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ParseEnumPipe
|
|
37
|
+
@Get()
|
|
38
|
+
findByStatus(
|
|
39
|
+
@Query('status', new ParseEnumPipe(UserStatus)) status: UserStatus,
|
|
40
|
+
) {
|
|
41
|
+
return this.usersService.findByStatus(status);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// DefaultValuePipe
|
|
45
|
+
@Get()
|
|
46
|
+
findAll(
|
|
47
|
+
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
|
|
48
|
+
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
|
|
49
|
+
) {
|
|
50
|
+
return this.usersService.findAll({ page, limit });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ParseArrayPipe
|
|
54
|
+
@Get()
|
|
55
|
+
findByIds(
|
|
56
|
+
@Query('ids', new ParseArrayPipe({ items: Number, separator: ',' }))
|
|
57
|
+
ids: number[],
|
|
58
|
+
) {
|
|
59
|
+
return this.usersService.findByIds(ids);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Global Validation Pipe
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// main.ts
|
|
68
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
69
|
+
|
|
70
|
+
async function bootstrap() {
|
|
71
|
+
const app = await NestFactory.create(AppModule);
|
|
72
|
+
|
|
73
|
+
app.useGlobalPipes(
|
|
74
|
+
new ValidationPipe({
|
|
75
|
+
whitelist: true, // Strip non-decorated properties
|
|
76
|
+
forbidNonWhitelisted: true, // Throw on extra properties
|
|
77
|
+
transform: true, // Transform payloads to DTO types
|
|
78
|
+
transformOptions: {
|
|
79
|
+
enableImplicitConversion: true,
|
|
80
|
+
},
|
|
81
|
+
exceptionFactory: (errors) => {
|
|
82
|
+
const messages = errors.map((error) => ({
|
|
83
|
+
field: error.property,
|
|
84
|
+
constraints: Object.values(error.constraints || {}),
|
|
85
|
+
}));
|
|
86
|
+
return new BadRequestException({ errors: messages });
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await app.listen(3000);
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Custom Pipes
|
|
96
|
+
|
|
97
|
+
### Transform Pipe
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// pipes/parse-date.pipe.ts
|
|
101
|
+
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
|
|
102
|
+
|
|
103
|
+
@Injectable()
|
|
104
|
+
export class ParseDatePipe implements PipeTransform<string, Date> {
|
|
105
|
+
transform(value: string): Date {
|
|
106
|
+
const date = new Date(value);
|
|
107
|
+
|
|
108
|
+
if (isNaN(date.getTime())) {
|
|
109
|
+
throw new BadRequestException(`Invalid date format: ${value}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return date;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Usage
|
|
117
|
+
@Get()
|
|
118
|
+
findByDate(@Query('date', ParseDatePipe) date: Date) {
|
|
119
|
+
return this.service.findByDate(date);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Validation Pipe with Schema
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// pipes/zod-validation.pipe.ts
|
|
127
|
+
import { PipeTransform, BadRequestException } from '@nestjs/common';
|
|
128
|
+
import { ZodSchema, ZodError } from 'zod';
|
|
129
|
+
|
|
130
|
+
export class ZodValidationPipe implements PipeTransform {
|
|
131
|
+
constructor(private readonly schema: ZodSchema) {}
|
|
132
|
+
|
|
133
|
+
transform(value: unknown) {
|
|
134
|
+
try {
|
|
135
|
+
return this.schema.parse(value);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error instanceof ZodError) {
|
|
138
|
+
throw new BadRequestException({
|
|
139
|
+
message: 'Validation failed',
|
|
140
|
+
errors: error.errors.map((e) => ({
|
|
141
|
+
path: e.path.join('.'),
|
|
142
|
+
message: e.message,
|
|
143
|
+
})),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Usage
|
|
152
|
+
const createUserSchema = z.object({
|
|
153
|
+
email: z.string().email(),
|
|
154
|
+
name: z.string().min(2),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
@Post()
|
|
158
|
+
create(@Body(new ZodValidationPipe(createUserSchema)) dto: CreateUserDto) {
|
|
159
|
+
return this.usersService.create(dto);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Async Pipe
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// pipes/user-exists.pipe.ts
|
|
167
|
+
import {
|
|
168
|
+
PipeTransform,
|
|
169
|
+
Injectable,
|
|
170
|
+
NotFoundException,
|
|
171
|
+
} from '@nestjs/common';
|
|
172
|
+
import { UsersService } from '../users.service';
|
|
173
|
+
|
|
174
|
+
@Injectable()
|
|
175
|
+
export class UserExistsPipe implements PipeTransform<string, Promise<User>> {
|
|
176
|
+
constructor(private readonly usersService: UsersService) {}
|
|
177
|
+
|
|
178
|
+
async transform(id: string): Promise<User> {
|
|
179
|
+
const user = await this.usersService.findOne(id);
|
|
180
|
+
|
|
181
|
+
if (!user) {
|
|
182
|
+
throw new NotFoundException(`User with ID ${id} not found`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return user;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Usage - returns User directly, not ID
|
|
190
|
+
@Get(':id')
|
|
191
|
+
findOne(@Param('id', UserExistsPipe) user: User) {
|
|
192
|
+
return user;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Trim and Sanitize
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// pipes/trim.pipe.ts
|
|
200
|
+
import { PipeTransform, Injectable } from '@nestjs/common';
|
|
201
|
+
|
|
202
|
+
@Injectable()
|
|
203
|
+
export class TrimPipe implements PipeTransform {
|
|
204
|
+
transform(value: unknown) {
|
|
205
|
+
if (typeof value === 'string') {
|
|
206
|
+
return value.trim();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (typeof value === 'object' && value !== null) {
|
|
210
|
+
return this.trimObject(value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private trimObject(obj: Record<string, unknown>): Record<string, unknown> {
|
|
217
|
+
const trimmed: Record<string, unknown> = {};
|
|
218
|
+
|
|
219
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
220
|
+
trimmed[key] = typeof val === 'string' ? val.trim() : val;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return trimmed;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### File Validation Pipe
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// pipes/file-validation.pipe.ts
|
|
232
|
+
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
|
|
233
|
+
|
|
234
|
+
interface FileValidationOptions {
|
|
235
|
+
maxSize: number;
|
|
236
|
+
allowedMimeTypes: string[];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@Injectable()
|
|
240
|
+
export class FileValidationPipe implements PipeTransform {
|
|
241
|
+
constructor(private readonly options: FileValidationOptions) {}
|
|
242
|
+
|
|
243
|
+
transform(file: Express.Multer.File) {
|
|
244
|
+
if (!file) {
|
|
245
|
+
throw new BadRequestException('File is required');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (file.size > this.options.maxSize) {
|
|
249
|
+
throw new BadRequestException(
|
|
250
|
+
`File size exceeds ${this.options.maxSize / 1024 / 1024}MB`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!this.options.allowedMimeTypes.includes(file.mimetype)) {
|
|
255
|
+
throw new BadRequestException(
|
|
256
|
+
`File type ${file.mimetype} is not allowed`,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return file;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Usage
|
|
265
|
+
@Post('upload')
|
|
266
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
267
|
+
upload(
|
|
268
|
+
@UploadedFile(
|
|
269
|
+
new FileValidationPipe({
|
|
270
|
+
maxSize: 5 * 1024 * 1024,
|
|
271
|
+
allowedMimeTypes: ['image/jpeg', 'image/png'],
|
|
272
|
+
}),
|
|
273
|
+
)
|
|
274
|
+
file: Express.Multer.File,
|
|
275
|
+
) {
|
|
276
|
+
return this.uploadService.upload(file);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Pipe Binding Scopes
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// Parameter level
|
|
284
|
+
@Get(':id')
|
|
285
|
+
findOne(@Param('id', ParseIntPipe) id: number) {}
|
|
286
|
+
|
|
287
|
+
// Method level
|
|
288
|
+
@Post()
|
|
289
|
+
@UsePipes(ValidationPipe)
|
|
290
|
+
create(@Body() dto: CreateDto) {}
|
|
291
|
+
|
|
292
|
+
// Controller level
|
|
293
|
+
@Controller('users')
|
|
294
|
+
@UsePipes(TrimPipe)
|
|
295
|
+
export class UsersController {}
|
|
296
|
+
|
|
297
|
+
// Global level (main.ts)
|
|
298
|
+
app.useGlobalPipes(new ValidationPipe());
|
|
299
|
+
|
|
300
|
+
// Global with DI
|
|
301
|
+
@Module({
|
|
302
|
+
providers: [
|
|
303
|
+
{
|
|
304
|
+
provide: APP_PIPE,
|
|
305
|
+
useClass: ValidationPipe,
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
})
|
|
309
|
+
export class AppModule {}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Anti-patterns
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
// BAD: Pipe with side effects
|
|
316
|
+
@Injectable()
|
|
317
|
+
export class LoggingPipe implements PipeTransform {
|
|
318
|
+
transform(value: unknown) {
|
|
319
|
+
console.log(value); // Use interceptor instead
|
|
320
|
+
this.analyticsService.track(value); // Side effect!
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// GOOD: Pipes are for transformation/validation only
|
|
326
|
+
|
|
327
|
+
// BAD: Heavy async operations in pipe
|
|
328
|
+
@Injectable()
|
|
329
|
+
export class HeavyPipe implements PipeTransform {
|
|
330
|
+
async transform(value: string) {
|
|
331
|
+
await this.externalApi.validate(value); // Too slow
|
|
332
|
+
return value;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// GOOD: Keep pipes lightweight, use guards for auth checks
|
|
337
|
+
|
|
338
|
+
// BAD: Not handling validation errors properly
|
|
339
|
+
transform(value: string) {
|
|
340
|
+
return new Date(value); // Crashes on invalid input
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// GOOD: Proper error handling
|
|
344
|
+
transform(value: string) {
|
|
345
|
+
const date = new Date(value);
|
|
346
|
+
if (isNaN(date.getTime())) {
|
|
347
|
+
throw new BadRequestException('Invalid date');
|
|
348
|
+
}
|
|
349
|
+
return date;
|
|
350
|
+
}
|
|
351
|
+
```
|