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