@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,300 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "src/common/**/*.ts"
|
|
4
|
+
- "src/**/*.decorator.ts"
|
|
5
|
+
- "src/**/*.filter.ts"
|
|
6
|
+
- "src/**/*.interceptor.ts"
|
|
7
|
+
- "src/**/*.pipe.ts"
|
|
8
|
+
- "src/main.ts"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# NestJS Common Patterns
|
|
12
|
+
|
|
13
|
+
## Global Setup (main.ts)
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { NestFactory } from '@nestjs/core';
|
|
17
|
+
import { ValidationPipe } from '@nestjs/common';
|
|
18
|
+
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
19
|
+
import { AppModule } from './app.module';
|
|
20
|
+
|
|
21
|
+
async function bootstrap() {
|
|
22
|
+
const app = await NestFactory.create(AppModule);
|
|
23
|
+
|
|
24
|
+
// Global validation pipe
|
|
25
|
+
app.useGlobalPipes(
|
|
26
|
+
new ValidationPipe({
|
|
27
|
+
whitelist: true,
|
|
28
|
+
forbidNonWhitelisted: true,
|
|
29
|
+
transform: true,
|
|
30
|
+
transformOptions: {
|
|
31
|
+
enableImplicitConversion: true,
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// Global prefix
|
|
37
|
+
app.setGlobalPrefix('api/v1');
|
|
38
|
+
|
|
39
|
+
// CORS
|
|
40
|
+
app.enableCors({
|
|
41
|
+
origin: process.env.CORS_ORIGIN?.split(',') || '*',
|
|
42
|
+
credentials: true,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Swagger (dev only)
|
|
46
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
47
|
+
const config = new DocumentBuilder()
|
|
48
|
+
.setTitle('API')
|
|
49
|
+
.setVersion('1.0')
|
|
50
|
+
.addBearerAuth()
|
|
51
|
+
.build();
|
|
52
|
+
const document = SwaggerModule.createDocument(app, config);
|
|
53
|
+
SwaggerModule.setup('docs', app, document);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await app.listen(process.env.PORT ?? 3000);
|
|
57
|
+
}
|
|
58
|
+
bootstrap();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Custom Decorators
|
|
62
|
+
|
|
63
|
+
### @CurrentUser Decorator
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// common/decorators/current-user.decorator.ts
|
|
67
|
+
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
68
|
+
|
|
69
|
+
export const CurrentUser = createParamDecorator(
|
|
70
|
+
(data: string | undefined, ctx: ExecutionContext) => {
|
|
71
|
+
const request = ctx.switchToHttp().getRequest();
|
|
72
|
+
const user = request.user;
|
|
73
|
+
return data ? user?.[data] : user;
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Usage
|
|
78
|
+
@Get('profile')
|
|
79
|
+
getProfile(@CurrentUser() user: User) {
|
|
80
|
+
return user;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Get('email')
|
|
84
|
+
getEmail(@CurrentUser('email') email: string) {
|
|
85
|
+
return { email };
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### @Public Decorator
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// common/decorators/public.decorator.ts
|
|
93
|
+
import { SetMetadata } from '@nestjs/common';
|
|
94
|
+
|
|
95
|
+
export const IS_PUBLIC_KEY = 'isPublic';
|
|
96
|
+
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
|
97
|
+
|
|
98
|
+
// Usage
|
|
99
|
+
@Public()
|
|
100
|
+
@Get('health')
|
|
101
|
+
healthCheck() {
|
|
102
|
+
return { status: 'ok' };
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### @Roles Decorator
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// common/decorators/roles.decorator.ts
|
|
110
|
+
import { SetMetadata } from '@nestjs/common';
|
|
111
|
+
|
|
112
|
+
export const ROLES_KEY = 'roles';
|
|
113
|
+
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
|
|
114
|
+
|
|
115
|
+
// Usage
|
|
116
|
+
@Roles('admin')
|
|
117
|
+
@Get('admin')
|
|
118
|
+
adminOnly() { ... }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Exception Filters
|
|
122
|
+
|
|
123
|
+
### Global Exception Filter
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
// common/filters/all-exceptions.filter.ts
|
|
127
|
+
import {
|
|
128
|
+
ExceptionFilter,
|
|
129
|
+
Catch,
|
|
130
|
+
ArgumentsHost,
|
|
131
|
+
HttpException,
|
|
132
|
+
HttpStatus,
|
|
133
|
+
Logger,
|
|
134
|
+
} from '@nestjs/common';
|
|
135
|
+
import { Request, Response } from 'express';
|
|
136
|
+
|
|
137
|
+
@Catch()
|
|
138
|
+
export class AllExceptionsFilter implements ExceptionFilter {
|
|
139
|
+
private readonly logger = new Logger(AllExceptionsFilter.name);
|
|
140
|
+
|
|
141
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
142
|
+
const ctx = host.switchToHttp();
|
|
143
|
+
const response = ctx.getResponse<Response>();
|
|
144
|
+
const request = ctx.getRequest<Request>();
|
|
145
|
+
|
|
146
|
+
const status =
|
|
147
|
+
exception instanceof HttpException
|
|
148
|
+
? exception.getStatus()
|
|
149
|
+
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
150
|
+
|
|
151
|
+
const message =
|
|
152
|
+
exception instanceof HttpException
|
|
153
|
+
? exception.message
|
|
154
|
+
: 'Internal server error';
|
|
155
|
+
|
|
156
|
+
// Log error
|
|
157
|
+
this.logger.error(
|
|
158
|
+
`${request.method} ${request.url} - ${status} - ${message}`,
|
|
159
|
+
exception instanceof Error ? exception.stack : undefined,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
response.status(status).json({
|
|
163
|
+
statusCode: status,
|
|
164
|
+
message,
|
|
165
|
+
timestamp: new Date().toISOString(),
|
|
166
|
+
path: request.url,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Register globally in main.ts
|
|
172
|
+
app.useGlobalFilters(new AllExceptionsFilter());
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Interceptors
|
|
176
|
+
|
|
177
|
+
### Transform Response Interceptor
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// common/interceptors/transform.interceptor.ts
|
|
181
|
+
import {
|
|
182
|
+
Injectable,
|
|
183
|
+
NestInterceptor,
|
|
184
|
+
ExecutionContext,
|
|
185
|
+
CallHandler,
|
|
186
|
+
} from '@nestjs/common';
|
|
187
|
+
import { Observable } from 'rxjs';
|
|
188
|
+
import { map } from 'rxjs/operators';
|
|
189
|
+
|
|
190
|
+
export interface Response<T> {
|
|
191
|
+
success: boolean;
|
|
192
|
+
data: T;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@Injectable()
|
|
196
|
+
export class TransformInterceptor<T>
|
|
197
|
+
implements NestInterceptor<T, Response<T>>
|
|
198
|
+
{
|
|
199
|
+
intercept(
|
|
200
|
+
context: ExecutionContext,
|
|
201
|
+
next: CallHandler,
|
|
202
|
+
): Observable<Response<T>> {
|
|
203
|
+
return next.handle().pipe(
|
|
204
|
+
map((data) => ({
|
|
205
|
+
success: true,
|
|
206
|
+
data,
|
|
207
|
+
})),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Logging Interceptor
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
// common/interceptors/logging.interceptor.ts
|
|
217
|
+
import {
|
|
218
|
+
Injectable,
|
|
219
|
+
NestInterceptor,
|
|
220
|
+
ExecutionContext,
|
|
221
|
+
CallHandler,
|
|
222
|
+
Logger,
|
|
223
|
+
} from '@nestjs/common';
|
|
224
|
+
import { Observable } from 'rxjs';
|
|
225
|
+
import { tap } from 'rxjs/operators';
|
|
226
|
+
|
|
227
|
+
@Injectable()
|
|
228
|
+
export class LoggingInterceptor implements NestInterceptor {
|
|
229
|
+
private readonly logger = new Logger(LoggingInterceptor.name);
|
|
230
|
+
|
|
231
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
232
|
+
const request = context.switchToHttp().getRequest();
|
|
233
|
+
const { method, url } = request;
|
|
234
|
+
const now = Date.now();
|
|
235
|
+
|
|
236
|
+
return next.handle().pipe(
|
|
237
|
+
tap(() => {
|
|
238
|
+
this.logger.log(`${method} ${url} - ${Date.now() - now}ms`);
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Guards
|
|
246
|
+
|
|
247
|
+
### Roles Guard
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// common/guards/roles.guard.ts
|
|
251
|
+
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
|
252
|
+
import { Reflector } from '@nestjs/core';
|
|
253
|
+
import { ROLES_KEY } from '../decorators/roles.decorator';
|
|
254
|
+
|
|
255
|
+
@Injectable()
|
|
256
|
+
export class RolesGuard implements CanActivate {
|
|
257
|
+
constructor(private reflector: Reflector) {}
|
|
258
|
+
|
|
259
|
+
canActivate(context: ExecutionContext): boolean {
|
|
260
|
+
const requiredRoles = this.reflector.getAllAndOverride<string[]>(
|
|
261
|
+
ROLES_KEY,
|
|
262
|
+
[context.getHandler(), context.getClass()],
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
if (!requiredRoles) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const { user } = context.switchToHttp().getRequest();
|
|
270
|
+
return requiredRoles.some((role) => user.roles?.includes(role));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Pipes
|
|
276
|
+
|
|
277
|
+
### Parse Optional Int Pipe
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
// common/pipes/parse-optional-int.pipe.ts
|
|
281
|
+
import { PipeTransform, Injectable } from '@nestjs/common';
|
|
282
|
+
|
|
283
|
+
@Injectable()
|
|
284
|
+
export class ParseOptionalIntPipe implements PipeTransform {
|
|
285
|
+
transform(value: string | undefined): number | undefined {
|
|
286
|
+
if (value === undefined || value === '') {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
const val = parseInt(value, 10);
|
|
290
|
+
return isNaN(val) ? undefined : val;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Usage
|
|
295
|
+
@Get()
|
|
296
|
+
findAll(
|
|
297
|
+
@Query('page', ParseOptionalIntPipe) page?: number,
|
|
298
|
+
@Query('limit', ParseOptionalIntPipe) limit?: number,
|
|
299
|
+
) { ... }
|
|
300
|
+
```
|
|
@@ -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
|
+
```
|