@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
package/configs/nestjs/CLAUDE.md
CHANGED
|
@@ -4,260 +4,102 @@
|
|
|
4
4
|
|
|
5
5
|
## Stack
|
|
6
6
|
|
|
7
|
-
- NestJS
|
|
7
|
+
- NestJS 11+
|
|
8
8
|
- TypeScript strict mode
|
|
9
9
|
- Node.js 20+
|
|
10
|
-
-
|
|
11
|
-
-
|
|
10
|
+
- Vitest + Supertest
|
|
11
|
+
- Prisma or TypeORM
|
|
12
12
|
|
|
13
|
-
## Architecture
|
|
14
|
-
|
|
15
|
-
### Modular Monolith (Recommended)
|
|
13
|
+
## Architecture - Modular Monolith
|
|
16
14
|
|
|
17
15
|
```
|
|
18
16
|
src/
|
|
19
17
|
├── modules/
|
|
20
|
-
│ ├──
|
|
21
|
-
│ │ ├──
|
|
22
|
-
│ │ ├──
|
|
23
|
-
│ │ ├──
|
|
18
|
+
│ ├── [feature]/
|
|
19
|
+
│ │ ├── [feature].module.ts
|
|
20
|
+
│ │ ├── [feature].controller.ts
|
|
21
|
+
│ │ ├── [feature].service.ts
|
|
22
|
+
│ │ ├── [feature].repository.ts
|
|
24
23
|
│ │ ├── dto/
|
|
25
|
-
│ │
|
|
26
|
-
│
|
|
27
|
-
│
|
|
28
|
-
│
|
|
29
|
-
│ │ └── users.repository.ts
|
|
30
|
-
│ │
|
|
31
|
-
│ ├── auth/
|
|
32
|
-
│ │ ├── auth.module.ts
|
|
33
|
-
│ │ ├── auth.controller.ts
|
|
34
|
-
│ │ ├── auth.service.ts
|
|
35
|
-
│ │ ├── strategies/
|
|
36
|
-
│ │ │ ├── jwt.strategy.ts
|
|
37
|
-
│ │ │ └── local.strategy.ts
|
|
38
|
-
│ │ └── guards/
|
|
39
|
-
│ │ └── jwt-auth.guard.ts
|
|
40
|
-
│ │
|
|
41
|
-
│ └── [feature]/
|
|
42
|
-
│ └── ...
|
|
43
|
-
│
|
|
24
|
+
│ │ └── entities/
|
|
25
|
+
│ └── auth/
|
|
26
|
+
│ ├── strategies/
|
|
27
|
+
│ └── guards/
|
|
44
28
|
├── common/
|
|
45
29
|
│ ├── decorators/
|
|
46
30
|
│ ├── filters/
|
|
47
31
|
│ ├── guards/
|
|
48
32
|
│ ├── interceptors/
|
|
49
33
|
│ └── pipes/
|
|
50
|
-
│
|
|
51
34
|
├── config/
|
|
52
|
-
│ ├── config.module.ts
|
|
53
|
-
│ └── database.config.ts
|
|
54
|
-
│
|
|
55
35
|
├── app.module.ts
|
|
56
36
|
└── main.ts
|
|
57
37
|
```
|
|
58
38
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
- **Single Responsibility**: Each module owns one domain
|
|
62
|
-
- **Clear Boundaries**: Modules communicate via exported services
|
|
63
|
-
- **In-Memory Communication**: Direct method calls, not HTTP between modules
|
|
64
|
-
- **Barrel Exports**: Use `index.ts` for clean imports
|
|
39
|
+
## Request Lifecycle
|
|
65
40
|
|
|
66
|
-
|
|
41
|
+
```
|
|
42
|
+
Request → Middleware → Guard → Interceptor (pre) → Pipe → Controller → Interceptor (post) → Response
|
|
43
|
+
```
|
|
67
44
|
|
|
68
|
-
|
|
45
|
+
## Core Principles
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
- Delegate business logic to services
|
|
72
|
-
- Use DTOs for all inputs
|
|
73
|
-
- Return consistent response shapes
|
|
47
|
+
### Module Design
|
|
74
48
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
constructor(private readonly usersService: UsersService) {}
|
|
49
|
+
- **Single Responsibility**: One module = one domain
|
|
50
|
+
- **Clear Boundaries**: Communicate via exported services only
|
|
51
|
+
- **Barrel Exports**: Use `index.ts` for clean imports
|
|
79
52
|
|
|
80
|
-
|
|
81
|
-
@HttpCode(HttpStatus.CREATED)
|
|
82
|
-
create(@Body() createUserDto: CreateUserDto): Promise<User> {
|
|
83
|
-
return this.usersService.create(createUserDto);
|
|
84
|
-
}
|
|
53
|
+
### Layer Responsibilities
|
|
85
54
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
55
|
+
| Layer | Responsibility |
|
|
56
|
+
|-------|---------------|
|
|
57
|
+
| Controller | HTTP only, delegates to service |
|
|
58
|
+
| Service | All business logic |
|
|
59
|
+
| Repository | Data access only |
|
|
60
|
+
| DTO | Validation with class-validator |
|
|
92
61
|
|
|
93
|
-
###
|
|
62
|
+
### Exception Handling
|
|
94
63
|
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
64
|
+
Use built-in NestJS exceptions:
|
|
65
|
+
- `NotFoundException` - 404
|
|
66
|
+
- `BadRequestException` - 400
|
|
67
|
+
- `UnauthorizedException` - 401
|
|
68
|
+
- `ForbiddenException` - 403
|
|
69
|
+
- `ConflictException` - 409
|
|
98
70
|
|
|
99
|
-
|
|
100
|
-
@Injectable()
|
|
101
|
-
export class UsersService {
|
|
102
|
-
constructor(private readonly usersRepository: UsersRepository) {}
|
|
71
|
+
### Validation (Global)
|
|
103
72
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
return user;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
```
|
|
73
|
+
Enable global `ValidationPipe` in main.ts with:
|
|
74
|
+
- `whitelist: true` - Strip non-whitelisted properties
|
|
75
|
+
- `forbidNonWhitelisted: true` - Throw on extra properties
|
|
76
|
+
- `transform: true` - Auto-transform to DTO types
|
|
113
77
|
|
|
114
78
|
### DTOs
|
|
115
79
|
|
|
116
80
|
- Always use class-validator decorators
|
|
117
|
-
-
|
|
118
|
-
-
|
|
81
|
+
- `PartialType`, `PickType`, `OmitType` for variants
|
|
82
|
+
- Combine with Swagger decorators
|
|
119
83
|
|
|
120
|
-
|
|
121
|
-
export class CreateUserDto {
|
|
122
|
-
@IsEmail()
|
|
123
|
-
@IsNotEmpty()
|
|
124
|
-
email: string;
|
|
84
|
+
## Authentication
|
|
125
85
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
@IsString()
|
|
131
|
-
@IsOptional()
|
|
132
|
-
name?: string;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export class UpdateUserDto extends PartialType(CreateUserDto) {}
|
|
136
|
-
```
|
|
86
|
+
- Passport.js + JWT strategy
|
|
87
|
+
- Global `JwtAuthGuard` with `@Public()` decorator for exceptions
|
|
88
|
+
- `@CurrentUser()` decorator for accessing user
|
|
137
89
|
|
|
138
90
|
## Commands
|
|
139
91
|
|
|
140
92
|
```bash
|
|
141
|
-
#
|
|
142
|
-
npm run
|
|
143
|
-
|
|
144
|
-
#
|
|
145
|
-
npm run
|
|
146
|
-
|
|
147
|
-
# Tests
|
|
148
|
-
npm run test # Unit tests
|
|
149
|
-
npm run test:watch # Watch mode
|
|
150
|
-
npm run test:cov # Coverage
|
|
151
|
-
npm run test:e2e # E2E tests
|
|
152
|
-
|
|
153
|
-
# Linting
|
|
154
|
-
npm run lint
|
|
155
|
-
npm run format
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
## Global Setup (main.ts)
|
|
159
|
-
|
|
160
|
-
```typescript
|
|
161
|
-
async function bootstrap() {
|
|
162
|
-
const app = await NestFactory.create(AppModule);
|
|
163
|
-
|
|
164
|
-
// Global validation pipe
|
|
165
|
-
app.useGlobalPipes(
|
|
166
|
-
new ValidationPipe({
|
|
167
|
-
whitelist: true,
|
|
168
|
-
forbidNonWhitelisted: true,
|
|
169
|
-
transform: true,
|
|
170
|
-
transformOptions: {
|
|
171
|
-
enableImplicitConversion: true,
|
|
172
|
-
},
|
|
173
|
-
}),
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
// Global prefix
|
|
177
|
-
app.setGlobalPrefix('api/v1');
|
|
178
|
-
|
|
179
|
-
// CORS
|
|
180
|
-
app.enableCors();
|
|
181
|
-
|
|
182
|
-
// Swagger (dev only)
|
|
183
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
184
|
-
const config = new DocumentBuilder()
|
|
185
|
-
.setTitle('API')
|
|
186
|
-
.setVersion('1.0')
|
|
187
|
-
.addBearerAuth()
|
|
188
|
-
.build();
|
|
189
|
-
const document = SwaggerModule.createDocument(app, config);
|
|
190
|
-
SwaggerModule.setup('docs', app, document);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
await app.listen(process.env.PORT ?? 3000);
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Common Patterns
|
|
198
|
-
|
|
199
|
-
### Custom Decorator
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
// current-user.decorator.ts
|
|
203
|
-
export const CurrentUser = createParamDecorator(
|
|
204
|
-
(data: keyof User | undefined, ctx: ExecutionContext) => {
|
|
205
|
-
const request = ctx.switchToHttp().getRequest();
|
|
206
|
-
const user = request.user;
|
|
207
|
-
return data ? user?.[data] : user;
|
|
208
|
-
},
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// Usage
|
|
212
|
-
@Get('profile')
|
|
213
|
-
getProfile(@CurrentUser() user: User) { ... }
|
|
93
|
+
npm run start:dev # Dev with watch
|
|
94
|
+
npm run build # Production build
|
|
95
|
+
npm run test # Unit tests
|
|
96
|
+
npm run test:e2e # E2E tests
|
|
97
|
+
npm run test:cov # Coverage
|
|
214
98
|
```
|
|
215
99
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
@Catch()
|
|
220
|
-
export class AllExceptionsFilter implements ExceptionFilter {
|
|
221
|
-
catch(exception: unknown, host: ArgumentsHost) {
|
|
222
|
-
const ctx = host.switchToHttp();
|
|
223
|
-
const response = ctx.getResponse<Response>();
|
|
224
|
-
|
|
225
|
-
const status =
|
|
226
|
-
exception instanceof HttpException
|
|
227
|
-
? exception.getStatus()
|
|
228
|
-
: HttpStatus.INTERNAL_SERVER_ERROR;
|
|
229
|
-
|
|
230
|
-
const message =
|
|
231
|
-
exception instanceof HttpException
|
|
232
|
-
? exception.message
|
|
233
|
-
: 'Internal server error';
|
|
234
|
-
|
|
235
|
-
response.status(status).json({
|
|
236
|
-
statusCode: status,
|
|
237
|
-
message,
|
|
238
|
-
timestamp: new Date().toISOString(),
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### Response Interceptor
|
|
100
|
+
## Code Style
|
|
245
101
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
{
|
|
251
|
-
intercept(
|
|
252
|
-
context: ExecutionContext,
|
|
253
|
-
next: CallHandler,
|
|
254
|
-
): Observable<Response<T>> {
|
|
255
|
-
return next.handle().pipe(
|
|
256
|
-
map((data) => ({
|
|
257
|
-
success: true,
|
|
258
|
-
data,
|
|
259
|
-
})),
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
```
|
|
102
|
+
- Constructor injection (not property injection)
|
|
103
|
+
- `readonly` for injected dependencies
|
|
104
|
+
- Async methods return `Promise<T>`
|
|
105
|
+
- Use `ParseUUIDPipe`, `ParseIntPipe` for params
|
|
@@ -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
|
+
```
|