ginskill-init 1.0.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 +77 -0
- package/agents/developer.md +56 -0
- package/agents/frontend-design.md +69 -0
- package/agents/mobile-reviewer.md +36 -0
- package/agents/review-code.md +49 -0
- package/agents/security-scanner.md +50 -0
- package/agents/tester.md +72 -0
- package/bin/cli.js +226 -0
- package/package.json +20 -0
- package/skills/ai-asset-generator/SKILL.md +255 -0
- package/skills/ai-asset-generator/docs/gen-image.md +274 -0
- package/skills/ai-asset-generator/docs/genvideo.md +341 -0
- package/skills/ai-asset-generator/docs/remove-background.md +19 -0
- package/skills/ai-asset-generator/generate-credit-assets.mjs +180 -0
- package/skills/ai-asset-generator/generate-ginbrowser-assets.mjs +242 -0
- package/skills/ai-asset-generator/generate-sty-icon.mjs +149 -0
- package/skills/ai-asset-generator/lib/bg-remove.mjs +34 -0
- package/skills/ai-asset-generator/lib/env.mjs +38 -0
- package/skills/ai-asset-generator/lib/kie-client.mjs +88 -0
- package/skills/ai-asset-generator/scripts/scaffold-generator.mjs +203 -0
- package/skills/ai-build-ai/SKILL.md +124 -0
- package/skills/ai-build-ai/docs/agent-teams.md +293 -0
- package/skills/ai-build-ai/docs/checkpointing.md +161 -0
- package/skills/ai-build-ai/docs/create-agent.md +399 -0
- package/skills/ai-build-ai/docs/create-mcp.md +395 -0
- package/skills/ai-build-ai/docs/create-skill.md +299 -0
- package/skills/ai-build-ai/docs/headless-mode.md +614 -0
- package/skills/ai-build-ai/docs/hooks.md +578 -0
- package/skills/ai-build-ai/docs/memory-claude-md.md +375 -0
- package/skills/ai-build-ai/docs/output-styles.md +208 -0
- package/skills/ai-build-ai/docs/overview.md +162 -0
- package/skills/ai-build-ai/docs/permissions.md +391 -0
- package/skills/ai-build-ai/docs/plugins.md +396 -0
- package/skills/ai-build-ai/docs/sandbox.md +262 -0
- package/skills/ai-build-ai/scripts/load-tutorial.sh +54 -0
- package/skills/icon-generator/SKILL.md +270 -0
- package/skills/mobile-app-review/SKILL.md +321 -0
- package/skills/mobile-app-review/references/apple-review.md +132 -0
- package/skills/mobile-app-review/references/google-play-review.md +203 -0
- package/skills/mongodb/SKILL.md +667 -0
- package/skills/mongodb/references/mongoose-patterns.md +368 -0
- package/skills/nestjs-architecture/SKILL.md +1086 -0
- package/skills/nestjs-architecture/references/advanced-patterns.md +590 -0
- package/skills/performance/SKILL.md +509 -0
- package/skills/react-fsd-architecture/SKILL.md +693 -0
- package/skills/react-fsd-architecture/references/fsd-patterns.md +747 -0
- package/skills/react-query/SKILL.md +685 -0
- package/skills/react-query/references/query-patterns.md +365 -0
- package/skills/review-code/SKILL.md +321 -0
- package/skills/review-code/references/clean-code-principles.md +395 -0
- package/skills/review-code/references/frontend-patterns.md +136 -0
- package/skills/review-code/references/nestjs-patterns.md +184 -0
- package/skills/review-code/scripts/check-module.sh +201 -0
- package/skills/review-code/scripts/deep-scan.sh +604 -0
- package/skills/review-code/scripts/dep-check.sh +522 -0
- package/skills/review-code/scripts/detect-duplicates.sh +466 -0
- package/skills/review-code/scripts/format-check.sh +577 -0
- package/skills/review-code/scripts/run-review.sh +167 -0
- package/skills/review-code/scripts/scan-codebase.sh +152 -0
- package/skills/security-scanner/SKILL.md +327 -0
- package/skills/security-scanner/references/nestjs-security.md +260 -0
- package/skills/security-scanner/references/nextjs-security.md +201 -0
- package/skills/security-scanner/references/react-native-security.md +199 -0
- package/skills/security-scanner/scripts/security-scan.sh +478 -0
- package/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/ui-ux-pro-max/scripts/search.py +114 -0
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nestjs-architecture
|
|
3
|
+
description: |
|
|
4
|
+
**NestJS Feature-Based Architecture**: Production patterns for organizing NestJS backends — feature modules, core infrastructure, shared utilities, guards, queues, events, error handling, and project structure.
|
|
5
|
+
- MANDATORY TRIGGERS: nestjs architecture, nestjs structure, nestjs module, nestjs feature module, nestjs project structure, nestjs folder structure, nestjs organize, nestjs boilerplate, nestjs scaffold, nestjs guard, nestjs interceptor, nestjs pipe, nestjs filter, nestjs exception, nestjs queue, nestjs event, nestjs event emitter, nestjs bull, nestjs redis, nestjs config, nestjs database module, nestjs health check, nestjs rate limit, nestjs middleware, nestjs core module, nestjs shared module, nestjs dto pattern, nestjs service pattern, nestjs controller pattern, nestjs processor, nestjs decorator, nestjs graceful shutdown, nestjs swagger setup, nestjs validation, nestjs monolith
|
|
6
|
+
- Use this skill whenever the user is setting up a NestJS project, creating new feature modules, organizing backend code, adding infrastructure (guards, queues, events, health checks), or reviewing NestJS architecture. Also trigger when discussing backend project structure, module boundaries, dependency injection patterns, or scaling NestJS applications.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# NestJS Feature-Based Architecture
|
|
10
|
+
|
|
11
|
+
Production-ready patterns for organizing NestJS backends using a feature-based modular structure. Covers project layout, core infrastructure, feature modules, shared utilities, guards, queues, events, error handling, and scaling.
|
|
12
|
+
|
|
13
|
+
## Core Mental Model
|
|
14
|
+
|
|
15
|
+
**Features are the organizing principle.** Group code by business domain (user, order, notification), not by technical layer (controllers/, services/, entities/). Each feature is a self-contained module that owns its routes, business logic, data access, DTOs, and schemas.
|
|
16
|
+
|
|
17
|
+
Key principles:
|
|
18
|
+
- A feature module should be deletable without breaking unrelated features
|
|
19
|
+
- Core infrastructure (database, cache, auth, logging) lives in `core/` — imported once at the root
|
|
20
|
+
- Shared utilities (decorators, pipes, DTOs) live in `shared/` — imported where needed
|
|
21
|
+
- Communication between features uses events, not direct service imports
|
|
22
|
+
- Every module explicitly declares its imports and exports
|
|
23
|
+
|
|
24
|
+
## Project Structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
src/
|
|
28
|
+
├── main.ts # Bootstrap, global pipes/filters/interceptors
|
|
29
|
+
├── app.module.ts # Root module — imports core + features
|
|
30
|
+
├── app.controller.ts # Root health/info endpoint
|
|
31
|
+
├── app.service.ts # Root service
|
|
32
|
+
│
|
|
33
|
+
├── core/ # Platform infrastructure (imported once)
|
|
34
|
+
│ ├── config/ # Environment & app configuration
|
|
35
|
+
│ │ ├── env.schema.ts # Zod/Joi validation for env vars
|
|
36
|
+
│ │ ├── app.config.ts # Static app constants
|
|
37
|
+
│ │ ├── jwt.config.ts # Token expiration settings
|
|
38
|
+
│ │ ├── cors.config.ts # CORS origins
|
|
39
|
+
│ │ ├── helmet.config.ts # Security headers
|
|
40
|
+
│ │ └── swagger.config.ts # API documentation setup
|
|
41
|
+
│ ├── database/ # Database connection & lifecycle
|
|
42
|
+
│ │ ├── database.module.ts # Mongoose/TypeORM connection
|
|
43
|
+
│ │ └── database-cleanup.service.ts
|
|
44
|
+
│ ├── redis/ # Cache & session management
|
|
45
|
+
│ │ ├── redis.module.ts # Global Redis provider
|
|
46
|
+
│ │ ├── redis.service.ts # Redis client wrapper
|
|
47
|
+
│ │ └── redis.constants.ts
|
|
48
|
+
│ ├── queue/ # Job queue infrastructure
|
|
49
|
+
│ │ ├── queue.module.ts # Bull/BullMQ configuration
|
|
50
|
+
│ │ ├── base-queue.service.ts # Abstract queue service
|
|
51
|
+
│ │ ├── base-processor.ts # Abstract job processor
|
|
52
|
+
│ │ └── queue.constants.ts # Retry, backoff, timeout defaults
|
|
53
|
+
│ ├── logger/ # Structured logging
|
|
54
|
+
│ │ └── logger.module.ts
|
|
55
|
+
│ ├── health/ # Health check endpoints
|
|
56
|
+
│ │ ├── health.controller.ts # /health endpoint
|
|
57
|
+
│ │ └── custom-disk.indicator.ts
|
|
58
|
+
│ ├── exception/ # Global error handling
|
|
59
|
+
│ │ ├── http-exception.filter.ts # Global exception filter
|
|
60
|
+
│ │ └── app.exception.ts # Custom exception hierarchy
|
|
61
|
+
│ └── scheduler/ # Cron job registration
|
|
62
|
+
│ ├── scheduler.module.ts
|
|
63
|
+
│ └── scheduler.service.ts
|
|
64
|
+
│
|
|
65
|
+
├── features/ # Business domain modules
|
|
66
|
+
│ ├── user/
|
|
67
|
+
│ │ ├── user.module.ts
|
|
68
|
+
│ │ ├── user.controller.ts
|
|
69
|
+
│ │ ├── user.service.ts
|
|
70
|
+
│ │ ├── user.event.ts # Event listeners
|
|
71
|
+
│ │ ├── dto/
|
|
72
|
+
│ │ │ ├── create-user.dto.ts
|
|
73
|
+
│ │ │ └── update-user.dto.ts
|
|
74
|
+
│ │ ├── entities/
|
|
75
|
+
│ │ │ └── user.schema.ts # Mongoose schema
|
|
76
|
+
│ │ ├── processors/ # Queue job handlers
|
|
77
|
+
│ │ │ └── user-deletion.processor.ts
|
|
78
|
+
│ │ └── services/ # Feature-specific sub-services
|
|
79
|
+
│ │ └── user-profile.service.ts
|
|
80
|
+
│ ├── auth/
|
|
81
|
+
│ ├── order/
|
|
82
|
+
│ ├── notification/
|
|
83
|
+
│ ├── media/
|
|
84
|
+
│ └── ...
|
|
85
|
+
│
|
|
86
|
+
├── shared/ # Cross-feature utilities
|
|
87
|
+
│ ├── decorators/ # Custom decorators
|
|
88
|
+
│ │ ├── pagination.decorator.ts
|
|
89
|
+
│ │ └── api-select.decorator.ts
|
|
90
|
+
│ ├── guards/ # Reusable guards
|
|
91
|
+
│ │ └── rate-limit.guard.ts
|
|
92
|
+
│ ├── pipes/ # Transform/validation pipes
|
|
93
|
+
│ │ ├── populate.pipe.ts
|
|
94
|
+
│ │ ├── select.pipe.ts
|
|
95
|
+
│ │ └── condition.pipe.ts
|
|
96
|
+
│ ├── dto/ # Shared DTOs
|
|
97
|
+
│ │ ├── pagination.dto.ts
|
|
98
|
+
│ │ └── delete-response.dto.ts
|
|
99
|
+
│ ├── schema/ # Shared schema mixins
|
|
100
|
+
│ │ ├── priority.schema.ts
|
|
101
|
+
│ │ └── thumbnail.schema.ts
|
|
102
|
+
│ ├── enum/ # Shared enumerations
|
|
103
|
+
│ ├── events/ # Domain event types
|
|
104
|
+
│ │ └── domain-event.ts
|
|
105
|
+
│ ├── utils/ # Pure utility functions
|
|
106
|
+
│ ├── validators/ # Custom class-validator rules
|
|
107
|
+
│ └── interfaces/ # Shared type interfaces
|
|
108
|
+
│
|
|
109
|
+
└── types/ # Global type declarations
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Core Infrastructure
|
|
113
|
+
|
|
114
|
+
### Config: Environment Validation with Zod
|
|
115
|
+
|
|
116
|
+
Validate **all** environment variables at startup. The app should crash immediately if config is invalid — not at runtime when a feature first reads a missing var.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// core/config/env.schema.ts
|
|
120
|
+
import { z } from 'zod'
|
|
121
|
+
|
|
122
|
+
export const envSchema = z.object({
|
|
123
|
+
// Server
|
|
124
|
+
NODE_ENV: z.enum(['development', 'staging', 'production']).default('development'),
|
|
125
|
+
PORT: z.coerce.number().default(3000),
|
|
126
|
+
|
|
127
|
+
// Database
|
|
128
|
+
MONGODB_CONNECTION_STRING: z.string().url(),
|
|
129
|
+
MONGODB_NAME: z.string().min(1),
|
|
130
|
+
|
|
131
|
+
// Redis
|
|
132
|
+
REDIS_HOST: z.string().default('localhost'),
|
|
133
|
+
REDIS_PORT: z.coerce.number().default(6379),
|
|
134
|
+
REDIS_PASSWORD: z.string().optional(),
|
|
135
|
+
|
|
136
|
+
// JWT
|
|
137
|
+
JWT_ACCESS_TOKEN_SECRET: z.string().min(32),
|
|
138
|
+
JWT_REFRESH_TOKEN_SECRET: z.string().min(32),
|
|
139
|
+
JWT_ACCESS_TOKEN_EXPIRY: z.string().default('15m'),
|
|
140
|
+
JWT_REFRESH_TOKEN_EXPIRY: z.string().default('7d'),
|
|
141
|
+
|
|
142
|
+
// Storage — conditional validation
|
|
143
|
+
STORAGE_TYPE: z.enum(['local', 's3']).default('local'),
|
|
144
|
+
AWS_S3_BUCKET: z.string().optional(),
|
|
145
|
+
AWS_S3_REGION: z.string().optional(),
|
|
146
|
+
AWS_ACCESS_KEY_ID: z.string().optional(),
|
|
147
|
+
AWS_SECRET_ACCESS_KEY: z.string().optional(),
|
|
148
|
+
|
|
149
|
+
// Feature flags
|
|
150
|
+
SWAGGER_ENABLED: z.coerce.boolean().default(true),
|
|
151
|
+
ENABLE_TERMINAL_LOGGER: z.coerce.boolean().default(true),
|
|
152
|
+
ENABLE_FILE_LOGGER: z.coerce.boolean().default(false),
|
|
153
|
+
}).refine(
|
|
154
|
+
(data) => data.STORAGE_TYPE !== 's3' || (data.AWS_S3_BUCKET && data.AWS_S3_REGION),
|
|
155
|
+
{ message: 'AWS_S3_BUCKET and AWS_S3_REGION required when STORAGE_TYPE=s3' },
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
export type EnvConfig = z.infer<typeof envSchema>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// main.ts — validate on startup
|
|
163
|
+
import { envSchema } from './core/config/env.schema'
|
|
164
|
+
|
|
165
|
+
const env = envSchema.safeParse(process.env)
|
|
166
|
+
if (!env.success) {
|
|
167
|
+
console.error('Invalid environment variables:', env.error.format())
|
|
168
|
+
process.exit(1)
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Database Module
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// core/database/database.module.ts
|
|
176
|
+
import { Module, Logger } from '@nestjs/common'
|
|
177
|
+
import { MongooseModule } from '@nestjs/mongoose'
|
|
178
|
+
import { ConfigService } from '@nestjs/config'
|
|
179
|
+
import mongoose from 'mongoose'
|
|
180
|
+
|
|
181
|
+
@Module({
|
|
182
|
+
imports: [
|
|
183
|
+
MongooseModule.forRootAsync({
|
|
184
|
+
inject: [ConfigService],
|
|
185
|
+
useFactory: (config: ConfigService) => {
|
|
186
|
+
const logger = new Logger('Database')
|
|
187
|
+
|
|
188
|
+
mongoose.connection.on('connecting', () => logger.log('Connecting to MongoDB...'))
|
|
189
|
+
mongoose.connection.on('connected', () => logger.log('MongoDB connected'))
|
|
190
|
+
mongoose.connection.on('error', (err) => logger.error('MongoDB error:', err))
|
|
191
|
+
mongoose.connection.on('disconnected', () => logger.warn('MongoDB disconnected'))
|
|
192
|
+
mongoose.connection.on('reconnected', () => logger.log('MongoDB reconnected'))
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
uri: config.get('MONGODB_CONNECTION_STRING'),
|
|
196
|
+
dbName: config.get('MONGODB_NAME'),
|
|
197
|
+
maxPoolSize: 10,
|
|
198
|
+
minPoolSize: 2,
|
|
199
|
+
serverSelectionTimeoutMS: 5000,
|
|
200
|
+
socketTimeoutMS: 45000,
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
],
|
|
205
|
+
})
|
|
206
|
+
export class DatabaseModule {}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Global Exception Filter
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
// core/exception/app.exception.ts
|
|
213
|
+
export class AppException extends Error {
|
|
214
|
+
constructor(
|
|
215
|
+
public readonly errorCode: string,
|
|
216
|
+
public readonly statusCode: number,
|
|
217
|
+
message: string,
|
|
218
|
+
public readonly details?: Record<string, any>,
|
|
219
|
+
) {
|
|
220
|
+
super(message)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
static from(error: unknown): AppException {
|
|
224
|
+
if (error instanceof AppException) return error
|
|
225
|
+
if (error instanceof HttpException) {
|
|
226
|
+
return new AppException('HTTP_ERROR', error.getStatus(), error.message)
|
|
227
|
+
}
|
|
228
|
+
return new AppException('INTERNAL_ERROR', 500, 'An unexpected error occurred')
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export class UnauthorizedException extends AppException {
|
|
233
|
+
constructor(message = 'Unauthorized') {
|
|
234
|
+
super('UNAUTHORIZED', 401, message)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export class NotFoundException extends AppException {
|
|
239
|
+
constructor(entity: string, id?: string) {
|
|
240
|
+
super('NOT_FOUND', 404, id ? `${entity} with id ${id} not found` : `${entity} not found`)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export class ValidationException extends AppException {
|
|
245
|
+
constructor(details: Record<string, any>) {
|
|
246
|
+
super('VALIDATION_ERROR', 422, 'Validation failed', details)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
// core/exception/http-exception.filter.ts
|
|
253
|
+
@Catch()
|
|
254
|
+
export class HttpExceptionFilter implements ExceptionFilter {
|
|
255
|
+
private readonly logger = new Logger('ExceptionFilter')
|
|
256
|
+
|
|
257
|
+
catch(exception: unknown, host: ArgumentsHost) {
|
|
258
|
+
const ctx = host.switchToHttp()
|
|
259
|
+
const response = ctx.getResponse<Response>()
|
|
260
|
+
const request = ctx.getRequest<Request>()
|
|
261
|
+
|
|
262
|
+
const appException = AppException.from(exception)
|
|
263
|
+
const isProd = process.env.NODE_ENV === 'production'
|
|
264
|
+
|
|
265
|
+
this.logger.error(`[${appException.errorCode}] ${appException.message}`, {
|
|
266
|
+
path: request.url,
|
|
267
|
+
method: request.method,
|
|
268
|
+
...(isProd ? {} : { stack: (exception as Error)?.stack }),
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
response.status(appException.statusCode).json({
|
|
272
|
+
error_code: appException.errorCode,
|
|
273
|
+
message: appException.message,
|
|
274
|
+
details: appException.details,
|
|
275
|
+
...(isProd ? {} : { stack: (exception as Error)?.stack }),
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Queue Infrastructure
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// core/queue/queue.constants.ts
|
|
285
|
+
export const QUEUE_DEFAULTS = {
|
|
286
|
+
attempts: 3,
|
|
287
|
+
backoff: { type: 'exponential' as const, delay: 2000 },
|
|
288
|
+
timeout: 30_000,
|
|
289
|
+
removeOnComplete: { count: 100 },
|
|
290
|
+
removeOnFail: { count: 500 },
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// core/queue/base-queue.service.ts
|
|
296
|
+
import { Queue, Job } from 'bull'
|
|
297
|
+
|
|
298
|
+
export abstract class BaseQueueService {
|
|
299
|
+
private dedupeCache = new Map<string, number>()
|
|
300
|
+
|
|
301
|
+
constructor(protected readonly queue: Queue) {}
|
|
302
|
+
|
|
303
|
+
async addJob<T>(name: string, data: T, opts?: { dedupeKey?: string; dedupeTtl?: number }) {
|
|
304
|
+
// Deduplication — prevent identical jobs within TTL window
|
|
305
|
+
if (opts?.dedupeKey) {
|
|
306
|
+
const lastRun = this.dedupeCache.get(opts.dedupeKey)
|
|
307
|
+
const ttl = opts.dedupeTtl ?? 60_000
|
|
308
|
+
if (lastRun && Date.now() - lastRun < ttl) return null
|
|
309
|
+
this.dedupeCache.set(opts.dedupeKey, Date.now())
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return this.queue.add(name, data, {
|
|
313
|
+
...QUEUE_DEFAULTS,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async getJobStatus(jobId: string) {
|
|
318
|
+
const job = await this.queue.getJob(jobId)
|
|
319
|
+
if (!job) return null
|
|
320
|
+
const state = await job.getState()
|
|
321
|
+
return { id: job.id, state, data: job.data, progress: job.progress() }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async getMetrics() {
|
|
325
|
+
const [completed, failed, waiting, active] = await Promise.all([
|
|
326
|
+
this.queue.getCompletedCount(),
|
|
327
|
+
this.queue.getFailedCount(),
|
|
328
|
+
this.queue.getWaitingCount(),
|
|
329
|
+
this.queue.getActiveCount(),
|
|
330
|
+
])
|
|
331
|
+
return { completed, failed, waiting, active }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async gracefulShutdown(timeoutMs = 30_000) {
|
|
335
|
+
await this.queue.pause(true)
|
|
336
|
+
const start = Date.now()
|
|
337
|
+
while (Date.now() - start < timeoutMs) {
|
|
338
|
+
const active = await this.queue.getActiveCount()
|
|
339
|
+
if (active === 0) break
|
|
340
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
341
|
+
}
|
|
342
|
+
await this.queue.close()
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Health Checks
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
// core/health/health.controller.ts
|
|
351
|
+
@Controller('health')
|
|
352
|
+
export class HealthController {
|
|
353
|
+
constructor(
|
|
354
|
+
private health: HealthCheckService,
|
|
355
|
+
private mongoose: MongooseHealthIndicator,
|
|
356
|
+
private disk: DiskHealthIndicator,
|
|
357
|
+
private memory: MemoryHealthIndicator,
|
|
358
|
+
) {}
|
|
359
|
+
|
|
360
|
+
@Get()
|
|
361
|
+
@HealthCheck()
|
|
362
|
+
check() {
|
|
363
|
+
const isProd = process.env.NODE_ENV === 'production'
|
|
364
|
+
return this.health.check([
|
|
365
|
+
() => this.mongoose.pingCheck('mongodb'),
|
|
366
|
+
() => this.disk.checkStorage('disk', {
|
|
367
|
+
path: '/',
|
|
368
|
+
thresholdPercent: isProd ? 0.75 : 0.90,
|
|
369
|
+
}),
|
|
370
|
+
() => this.memory.checkHeap('memory_heap', 300 * 1024 * 1024),
|
|
371
|
+
])
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Rate Limiting with Redis
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
// shared/guards/rate-limit.guard.ts
|
|
380
|
+
@Injectable()
|
|
381
|
+
export class RateLimitGuard implements CanActivate {
|
|
382
|
+
constructor(private readonly redisService: RedisService) {}
|
|
383
|
+
|
|
384
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
385
|
+
const request = context.switchToHttp().getRequest()
|
|
386
|
+
const userId = request.user?.id ?? this.getGuestId(request)
|
|
387
|
+
const key = `rate:${userId}`
|
|
388
|
+
const window = 60 // seconds
|
|
389
|
+
const maxRequests = 100
|
|
390
|
+
|
|
391
|
+
// Atomic sliding window via Lua script
|
|
392
|
+
const script = `
|
|
393
|
+
local key = KEYS[1]
|
|
394
|
+
local now = tonumber(ARGV[1])
|
|
395
|
+
local window = tonumber(ARGV[2])
|
|
396
|
+
local max = tonumber(ARGV[3])
|
|
397
|
+
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
|
|
398
|
+
local count = redis.call('ZCARD', key)
|
|
399
|
+
if count >= max then return 0 end
|
|
400
|
+
redis.call('ZADD', key, now, now .. ':' .. math.random())
|
|
401
|
+
redis.call('EXPIRE', key, window)
|
|
402
|
+
return 1
|
|
403
|
+
`
|
|
404
|
+
|
|
405
|
+
const allowed = await this.redisService.eval(script, 1, key, Date.now(), window, maxRequests)
|
|
406
|
+
if (!allowed) throw new HttpException('Too Many Requests', 429)
|
|
407
|
+
return true
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private getGuestId(req: Request): string {
|
|
411
|
+
const ip = req.ip || req.socket.remoteAddress
|
|
412
|
+
const ua = req.headers['user-agent'] || ''
|
|
413
|
+
return `guest:${createHash('sha256').update(`${ip}:${ua}`).digest('hex').slice(0, 16)}`
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Feature Module Pattern
|
|
419
|
+
|
|
420
|
+
### Anatomy of a Feature
|
|
421
|
+
|
|
422
|
+
Every feature module follows the same structure:
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
features/order/
|
|
426
|
+
├── order.module.ts # Module — imports, providers, exports
|
|
427
|
+
├── order.controller.ts # HTTP routes
|
|
428
|
+
├── order.service.ts # Business logic
|
|
429
|
+
├── order.event.ts # Event listeners (@OnEvent)
|
|
430
|
+
├── dto/
|
|
431
|
+
│ ├── create-order.dto.ts # Input validation
|
|
432
|
+
│ ├── update-order.dto.ts
|
|
433
|
+
│ └── order-response.dto.ts # Output shape
|
|
434
|
+
├── entities/
|
|
435
|
+
│ └── order.schema.ts # Mongoose schema
|
|
436
|
+
├── processors/
|
|
437
|
+
│ └── order-fulfillment.processor.ts # Queue job handler
|
|
438
|
+
└── services/
|
|
439
|
+
├── order-pricing.service.ts # Sub-service for complex logic
|
|
440
|
+
└── order-notification.service.ts
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Module Definition
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
// features/order/order.module.ts
|
|
447
|
+
@Module({
|
|
448
|
+
imports: [
|
|
449
|
+
MongooseModule.forFeature([{ name: Order.name, schema: OrderSchema }]),
|
|
450
|
+
BullModule.registerQueue({ name: 'order-fulfillment' }),
|
|
451
|
+
forwardRef(() => UserModule), // circular dependency resolution
|
|
452
|
+
],
|
|
453
|
+
controllers: [OrderController],
|
|
454
|
+
providers: [
|
|
455
|
+
OrderService,
|
|
456
|
+
OrderPricingService,
|
|
457
|
+
OrderNotificationService,
|
|
458
|
+
OrderFulfillmentProcessor,
|
|
459
|
+
],
|
|
460
|
+
exports: [OrderService], // only export what other modules need
|
|
461
|
+
})
|
|
462
|
+
export class OrderModule {}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### Controller Pattern
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
// features/order/order.controller.ts
|
|
469
|
+
@ApiTags('Orders')
|
|
470
|
+
@Controller('orders')
|
|
471
|
+
@UseGuards(HybridAuthGuard, RolesGuard)
|
|
472
|
+
export class OrderController {
|
|
473
|
+
constructor(private readonly orderService: OrderService) {}
|
|
474
|
+
|
|
475
|
+
@Get()
|
|
476
|
+
@Auth(Role.USER)
|
|
477
|
+
@ApiPagination()
|
|
478
|
+
async findAll(
|
|
479
|
+
@CurrentUser() user: UserDocument,
|
|
480
|
+
@GetPagination() pagination: PaginationDto,
|
|
481
|
+
) {
|
|
482
|
+
return this.orderService.findAll(user.id, pagination)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
@Get(':id')
|
|
486
|
+
@Auth(Role.USER)
|
|
487
|
+
async findOne(
|
|
488
|
+
@CurrentUser() user: UserDocument,
|
|
489
|
+
@Param('id') id: string,
|
|
490
|
+
) {
|
|
491
|
+
return this.orderService.findOneOrFail(id, user.id)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
@Post()
|
|
495
|
+
@Auth(Role.USER)
|
|
496
|
+
async create(
|
|
497
|
+
@CurrentUser() user: UserDocument,
|
|
498
|
+
@Body() dto: CreateOrderDto,
|
|
499
|
+
) {
|
|
500
|
+
return this.orderService.create(user.id, dto)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
@Patch(':id')
|
|
504
|
+
@Auth(Role.USER)
|
|
505
|
+
async update(
|
|
506
|
+
@Param('id') id: string,
|
|
507
|
+
@CurrentUser() user: UserDocument,
|
|
508
|
+
@Body() dto: UpdateOrderDto,
|
|
509
|
+
) {
|
|
510
|
+
return this.orderService.update(id, user.id, dto)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
@Delete(':id')
|
|
514
|
+
@Auth(Role.USER)
|
|
515
|
+
@HttpCode(HttpStatus.NO_CONTENT)
|
|
516
|
+
async remove(
|
|
517
|
+
@Param('id') id: string,
|
|
518
|
+
@CurrentUser() user: UserDocument,
|
|
519
|
+
) {
|
|
520
|
+
return this.orderService.remove(id, user.id)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Service Pattern
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// features/order/order.service.ts
|
|
529
|
+
@Injectable()
|
|
530
|
+
export class OrderService {
|
|
531
|
+
constructor(
|
|
532
|
+
@InjectModel(Order.name) private orderModel: Model<OrderDocument>,
|
|
533
|
+
private readonly pricingService: OrderPricingService,
|
|
534
|
+
private readonly eventEmitter: EventEmitter2,
|
|
535
|
+
) {}
|
|
536
|
+
|
|
537
|
+
async findAll(userId: string, pagination: PaginationDto): Promise<PaginatedResponse<Order>> {
|
|
538
|
+
const { skip, limit, sort } = pagination
|
|
539
|
+
const filter = { userId: new Types.ObjectId(userId) }
|
|
540
|
+
|
|
541
|
+
const [docs, totalDocs] = await Promise.all([
|
|
542
|
+
this.orderModel.find(filter).sort(sort).skip(skip).limit(limit).lean().exec(),
|
|
543
|
+
this.orderModel.countDocuments(filter),
|
|
544
|
+
])
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
data: docs,
|
|
548
|
+
meta: new PageMeta({ totalDocs, page: Math.floor(skip / limit) + 1, limit }),
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async findOneOrFail(id: string, userId: string): Promise<Order> {
|
|
553
|
+
const order = await this.orderModel
|
|
554
|
+
.findOne({ _id: id, userId: new Types.ObjectId(userId) })
|
|
555
|
+
.lean()
|
|
556
|
+
.exec()
|
|
557
|
+
if (!order) throw new NotFoundException('Order', id)
|
|
558
|
+
return order
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async create(userId: string, dto: CreateOrderDto): Promise<Order> {
|
|
562
|
+
const total = await this.pricingService.calculate(dto.items)
|
|
563
|
+
const order = await this.orderModel.create({
|
|
564
|
+
userId: new Types.ObjectId(userId),
|
|
565
|
+
...dto,
|
|
566
|
+
total,
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
this.eventEmitter.emit('order.created', { orderId: order._id, userId })
|
|
570
|
+
return order.toObject()
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async remove(id: string, userId: string): Promise<void> {
|
|
574
|
+
const result = await this.orderModel.deleteOne({
|
|
575
|
+
_id: id,
|
|
576
|
+
userId: new Types.ObjectId(userId),
|
|
577
|
+
})
|
|
578
|
+
if (result.deletedCount === 0) throw new NotFoundException('Order', id)
|
|
579
|
+
this.eventEmitter.emit('order.deleted', { orderId: id, userId })
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### DTO Pattern (class-validator + class-transformer)
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
// features/order/dto/create-order.dto.ts
|
|
588
|
+
import { IsString, IsArray, IsNumber, ValidateNested, Min, ArrayMinSize } from 'class-validator'
|
|
589
|
+
import { Type } from 'class-transformer'
|
|
590
|
+
import { ApiProperty } from '@nestjs/swagger'
|
|
591
|
+
|
|
592
|
+
class OrderItemDto {
|
|
593
|
+
@ApiProperty()
|
|
594
|
+
@IsString()
|
|
595
|
+
productId: string
|
|
596
|
+
|
|
597
|
+
@ApiProperty()
|
|
598
|
+
@IsNumber()
|
|
599
|
+
@Min(1)
|
|
600
|
+
quantity: number
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export class CreateOrderDto {
|
|
604
|
+
@ApiProperty({ type: [OrderItemDto] })
|
|
605
|
+
@IsArray()
|
|
606
|
+
@ArrayMinSize(1)
|
|
607
|
+
@ValidateNested({ each: true })
|
|
608
|
+
@Type(() => OrderItemDto)
|
|
609
|
+
items: OrderItemDto[]
|
|
610
|
+
|
|
611
|
+
@ApiProperty({ required: false })
|
|
612
|
+
@IsString()
|
|
613
|
+
@IsOptional()
|
|
614
|
+
note?: string
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
// features/order/dto/update-order.dto.ts
|
|
620
|
+
import { PartialType } from '@nestjs/swagger'
|
|
621
|
+
import { CreateOrderDto } from './create-order.dto'
|
|
622
|
+
|
|
623
|
+
export class UpdateOrderDto extends PartialType(CreateOrderDto) {}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Schema Pattern (Mongoose + NestJS)
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
// features/order/entities/order.schema.ts
|
|
630
|
+
@Schema({
|
|
631
|
+
timestamps: true,
|
|
632
|
+
toJSON: { virtuals: true },
|
|
633
|
+
toObject: { virtuals: true },
|
|
634
|
+
})
|
|
635
|
+
export class Order {
|
|
636
|
+
@Prop({ type: Types.ObjectId, ref: 'User', required: true, index: true })
|
|
637
|
+
userId: Types.ObjectId
|
|
638
|
+
|
|
639
|
+
@Prop({
|
|
640
|
+
type: [{
|
|
641
|
+
productId: { type: Types.ObjectId, ref: 'Product', required: true },
|
|
642
|
+
quantity: { type: Number, required: true, min: 1 },
|
|
643
|
+
price: { type: Number, required: true },
|
|
644
|
+
}],
|
|
645
|
+
required: true,
|
|
646
|
+
})
|
|
647
|
+
items: Array<{ productId: Types.ObjectId; quantity: number; price: number }>
|
|
648
|
+
|
|
649
|
+
@Prop({ required: true })
|
|
650
|
+
total: number
|
|
651
|
+
|
|
652
|
+
@Prop({ type: String, enum: ['pending', 'confirmed', 'shipped', 'delivered', 'cancelled'], default: 'pending', index: true })
|
|
653
|
+
status: string
|
|
654
|
+
|
|
655
|
+
@Prop()
|
|
656
|
+
note?: string
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export type OrderDocument = HydratedDocument<Order>
|
|
660
|
+
export const OrderSchema = SchemaFactory.createForClass(Order)
|
|
661
|
+
|
|
662
|
+
// Compound indexes
|
|
663
|
+
OrderSchema.index({ userId: 1, status: 1, createdAt: -1 })
|
|
664
|
+
OrderSchema.index({ status: 1, createdAt: -1 })
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
## Event-Driven Communication
|
|
668
|
+
|
|
669
|
+
### Domain Events
|
|
670
|
+
|
|
671
|
+
Features communicate through events — never import another feature's service directly for side effects.
|
|
672
|
+
|
|
673
|
+
```typescript
|
|
674
|
+
// shared/events/domain-event.ts
|
|
675
|
+
export enum DomainEvent {
|
|
676
|
+
// Entity lifecycle
|
|
677
|
+
ORDER_CREATED = 'order.created',
|
|
678
|
+
ORDER_DELETED = 'order.deleted',
|
|
679
|
+
USER_DELETED = 'user.deleted',
|
|
680
|
+
|
|
681
|
+
// Cascade cleanup
|
|
682
|
+
CATEGORY_DELETED = 'category.deleted',
|
|
683
|
+
|
|
684
|
+
// Async processing
|
|
685
|
+
VECTOR_SYNC_REQUIRED = 'vector.sync.required',
|
|
686
|
+
NOTIFICATION_SEND = 'notification.send',
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
// features/order/order.event.ts
|
|
692
|
+
@Injectable()
|
|
693
|
+
export class OrderEventListener {
|
|
694
|
+
constructor(
|
|
695
|
+
@InjectModel(Order.name) private orderModel: Model<OrderDocument>,
|
|
696
|
+
) {}
|
|
697
|
+
|
|
698
|
+
@OnEvent(DomainEvent.USER_DELETED, { promisify: true })
|
|
699
|
+
async handleUserDeleted(payload: { userId: string }) {
|
|
700
|
+
await this.orderModel.deleteMany({ userId: new Types.ObjectId(payload.userId) })
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### When to Use Events vs Direct Imports
|
|
706
|
+
|
|
707
|
+
| Scenario | Approach | Why |
|
|
708
|
+
|----------|----------|-----|
|
|
709
|
+
| Feature A reacts to Feature B's action | **Event** | No coupling, Feature B doesn't know about A |
|
|
710
|
+
| Cascading delete (user deleted → delete orders) | **Event** | Each feature owns its cleanup |
|
|
711
|
+
| Need return value from another service | **Direct import** | Events are fire-and-forget |
|
|
712
|
+
| Shared read-only data (lookup, validation) | **Direct import** (export service) | Simple dependency |
|
|
713
|
+
| Async processing (send email, generate image) | **Event → Queue** | Decouple + retry |
|
|
714
|
+
|
|
715
|
+
## Queue Processing
|
|
716
|
+
|
|
717
|
+
### Processor Pattern
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
// features/order/processors/order-fulfillment.processor.ts
|
|
721
|
+
@Processor('order-fulfillment')
|
|
722
|
+
export class OrderFulfillmentProcessor extends BaseProcessor {
|
|
723
|
+
constructor(
|
|
724
|
+
private readonly orderService: OrderService,
|
|
725
|
+
private readonly notificationService: NotificationService,
|
|
726
|
+
) {
|
|
727
|
+
super()
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
@Process('fulfill')
|
|
731
|
+
async handleFulfillment(job: Job<{ orderId: string }>) {
|
|
732
|
+
const { orderId } = job.data
|
|
733
|
+
this.logger.log(`Processing order fulfillment: ${orderId}`)
|
|
734
|
+
|
|
735
|
+
await job.progress(10)
|
|
736
|
+
const order = await this.orderService.findById(orderId)
|
|
737
|
+
|
|
738
|
+
await job.progress(50)
|
|
739
|
+
await this.orderService.updateStatus(orderId, 'confirmed')
|
|
740
|
+
|
|
741
|
+
await job.progress(90)
|
|
742
|
+
await this.notificationService.send(order.userId, 'Order confirmed')
|
|
743
|
+
|
|
744
|
+
await job.progress(100)
|
|
745
|
+
return { orderId, status: 'confirmed' }
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### Dispatching Jobs
|
|
751
|
+
|
|
752
|
+
```typescript
|
|
753
|
+
// In service — dispatch async work to queue
|
|
754
|
+
async create(userId: string, dto: CreateOrderDto): Promise<Order> {
|
|
755
|
+
const order = await this.orderModel.create({ userId, ...dto })
|
|
756
|
+
|
|
757
|
+
await this.orderQueueService.addJob('fulfill', { orderId: order._id.toString() }, {
|
|
758
|
+
dedupeKey: `fulfill:${order._id}`,
|
|
759
|
+
dedupeTtl: 60_000,
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
return order.toObject()
|
|
763
|
+
}
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
## Shared Infrastructure
|
|
767
|
+
|
|
768
|
+
### Pagination Decorator + DTO
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
// shared/decorators/pagination.decorator.ts
|
|
772
|
+
export const GetPagination = createParamDecorator(
|
|
773
|
+
(data: unknown, ctx: ExecutionContext): PaginationDto => {
|
|
774
|
+
const request = ctx.switchToHttp().getRequest()
|
|
775
|
+
const { skip = 0, limit = 20, sort, search } = request.query
|
|
776
|
+
|
|
777
|
+
return {
|
|
778
|
+
skip: Math.max(0, Number(skip)),
|
|
779
|
+
limit: Math.min(100, Math.max(1, Number(limit))),
|
|
780
|
+
sort: parseSort(sort as string), // '-createdAt' → { createdAt: -1 }
|
|
781
|
+
search: parseSearch(search as string), // 'name:john' → { name: /john/i }
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
function parseSort(raw?: string): Record<string, 1 | -1> {
|
|
787
|
+
if (!raw) return { createdAt: -1 }
|
|
788
|
+
const dir = raw.startsWith('-') ? 1 : -1 // '-' = ascending, default = descending
|
|
789
|
+
const field = raw.replace(/^[+-]/, '')
|
|
790
|
+
return { [field]: dir }
|
|
791
|
+
}
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
// shared/dto/pagination.dto.ts
|
|
796
|
+
export class PageMeta {
|
|
797
|
+
totalDocs: number
|
|
798
|
+
page: number
|
|
799
|
+
limit: number
|
|
800
|
+
totalPages: number
|
|
801
|
+
hasNextPage: boolean
|
|
802
|
+
hasPrevPage: boolean
|
|
803
|
+
|
|
804
|
+
constructor(opts: { totalDocs: number; page: number; limit: number }) {
|
|
805
|
+
this.totalDocs = opts.totalDocs
|
|
806
|
+
this.page = opts.page
|
|
807
|
+
this.limit = opts.limit
|
|
808
|
+
this.totalPages = Math.ceil(opts.totalDocs / opts.limit)
|
|
809
|
+
this.hasNextPage = opts.page < this.totalPages
|
|
810
|
+
this.hasPrevPage = opts.page > 1
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export class PaginatedResponse<T> {
|
|
815
|
+
data: T[]
|
|
816
|
+
meta: PageMeta
|
|
817
|
+
}
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
### Shared Schema Mixins
|
|
821
|
+
|
|
822
|
+
```typescript
|
|
823
|
+
// shared/schema/priority.schema.ts
|
|
824
|
+
export const PriorityMixin = {
|
|
825
|
+
priority: { type: Number, default: 0, index: true },
|
|
826
|
+
sortOrder: { type: Number, default: 0 },
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Usage in any feature schema:
|
|
830
|
+
@Schema({ timestamps: true })
|
|
831
|
+
export class Category {
|
|
832
|
+
@Prop({ required: true })
|
|
833
|
+
name: string
|
|
834
|
+
|
|
835
|
+
@Prop({ default: 0, index: true })
|
|
836
|
+
priority: number
|
|
837
|
+
}
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
## Bootstrap (main.ts)
|
|
841
|
+
|
|
842
|
+
```typescript
|
|
843
|
+
// main.ts
|
|
844
|
+
async function bootstrap() {
|
|
845
|
+
// Validate environment first
|
|
846
|
+
const env = envSchema.safeParse(process.env)
|
|
847
|
+
if (!env.success) {
|
|
848
|
+
console.error('Invalid env vars:', env.error.format())
|
|
849
|
+
process.exit(1)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const app = await NestFactory.create(AppModule)
|
|
853
|
+
|
|
854
|
+
// Global prefix
|
|
855
|
+
app.setGlobalPrefix('api/v1', { exclude: ['health'] })
|
|
856
|
+
|
|
857
|
+
// Security
|
|
858
|
+
app.use(helmet(helmetConfig))
|
|
859
|
+
app.enableCors(corsConfig)
|
|
860
|
+
|
|
861
|
+
// Global pipes
|
|
862
|
+
app.useGlobalPipes(new ValidationPipe({
|
|
863
|
+
transform: true,
|
|
864
|
+
whitelist: true,
|
|
865
|
+
forbidNonWhitelisted: true,
|
|
866
|
+
transformOptions: { enableImplicitConversion: true },
|
|
867
|
+
}))
|
|
868
|
+
|
|
869
|
+
// Global filters
|
|
870
|
+
app.useGlobalFilters(new HttpExceptionFilter())
|
|
871
|
+
|
|
872
|
+
// Swagger
|
|
873
|
+
if (process.env.SWAGGER_ENABLED === 'true') {
|
|
874
|
+
setupSwagger(app)
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Graceful shutdown
|
|
878
|
+
app.enableShutdownHooks()
|
|
879
|
+
|
|
880
|
+
await app.listen(process.env.PORT || 3000)
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
bootstrap()
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
## Root Module
|
|
887
|
+
|
|
888
|
+
```typescript
|
|
889
|
+
// app.module.ts
|
|
890
|
+
@Module({
|
|
891
|
+
imports: [
|
|
892
|
+
// Core infrastructure (order matters for dependencies)
|
|
893
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
894
|
+
DatabaseModule,
|
|
895
|
+
RedisModule,
|
|
896
|
+
LoggerModule,
|
|
897
|
+
HealthModule,
|
|
898
|
+
QueueModule,
|
|
899
|
+
SchedulerModule,
|
|
900
|
+
EventEmitterModule.forRoot({ wildcard: true }),
|
|
901
|
+
CacheModule.registerAsync({
|
|
902
|
+
isGlobal: true,
|
|
903
|
+
inject: [ConfigService],
|
|
904
|
+
useFactory: (config: ConfigService) => ({
|
|
905
|
+
store: redisStore,
|
|
906
|
+
host: config.get('REDIS_HOST'),
|
|
907
|
+
port: config.get('REDIS_PORT'),
|
|
908
|
+
ttl: 60,
|
|
909
|
+
}),
|
|
910
|
+
}),
|
|
911
|
+
|
|
912
|
+
// Feature modules
|
|
913
|
+
AuthModule,
|
|
914
|
+
UserModule,
|
|
915
|
+
OrderModule,
|
|
916
|
+
NotificationModule,
|
|
917
|
+
MediaModule,
|
|
918
|
+
// ... other features
|
|
919
|
+
],
|
|
920
|
+
controllers: [AppController],
|
|
921
|
+
providers: [AppService],
|
|
922
|
+
})
|
|
923
|
+
export class AppModule {}
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
## Common Anti-Patterns
|
|
927
|
+
|
|
928
|
+
### 1. Organizing by Technical Layer
|
|
929
|
+
|
|
930
|
+
```
|
|
931
|
+
# BAD — "technical layer" structure
|
|
932
|
+
src/
|
|
933
|
+
├── controllers/
|
|
934
|
+
│ ├── user.controller.ts
|
|
935
|
+
│ ├── order.controller.ts
|
|
936
|
+
│ └── product.controller.ts
|
|
937
|
+
├── services/
|
|
938
|
+
│ ├── user.service.ts
|
|
939
|
+
│ ├── order.service.ts
|
|
940
|
+
│ └── product.service.ts
|
|
941
|
+
├── entities/
|
|
942
|
+
│ ├── user.schema.ts
|
|
943
|
+
│ └── order.schema.ts
|
|
944
|
+
|
|
945
|
+
# GOOD — "feature-based" structure
|
|
946
|
+
src/features/
|
|
947
|
+
├── user/
|
|
948
|
+
│ ├── user.controller.ts
|
|
949
|
+
│ ├── user.service.ts
|
|
950
|
+
│ └── entities/user.schema.ts
|
|
951
|
+
├── order/
|
|
952
|
+
│ ├── order.controller.ts
|
|
953
|
+
│ ├── order.service.ts
|
|
954
|
+
│ └── entities/order.schema.ts
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
### 2. God Module (Everything in AppModule)
|
|
958
|
+
|
|
959
|
+
```typescript
|
|
960
|
+
// BAD — all providers in root module
|
|
961
|
+
@Module({
|
|
962
|
+
providers: [UserService, OrderService, ProductService, EmailService, ...50 more],
|
|
963
|
+
})
|
|
964
|
+
export class AppModule {}
|
|
965
|
+
|
|
966
|
+
// GOOD — each feature is its own module
|
|
967
|
+
@Module({
|
|
968
|
+
imports: [UserModule, OrderModule, ProductModule],
|
|
969
|
+
})
|
|
970
|
+
export class AppModule {}
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
### 3. Cross-Feature Direct Service Imports for Side Effects
|
|
974
|
+
|
|
975
|
+
```typescript
|
|
976
|
+
// BAD — tight coupling: OrderService directly calls NotificationService
|
|
977
|
+
@Injectable()
|
|
978
|
+
export class OrderService {
|
|
979
|
+
constructor(private notificationService: NotificationService) {}
|
|
980
|
+
|
|
981
|
+
async create(dto) {
|
|
982
|
+
const order = await this.orderModel.create(dto)
|
|
983
|
+
await this.notificationService.sendOrderConfirmation(order) // tight coupling
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// GOOD — emit event, let NotificationModule listen
|
|
988
|
+
@Injectable()
|
|
989
|
+
export class OrderService {
|
|
990
|
+
constructor(private eventEmitter: EventEmitter2) {}
|
|
991
|
+
|
|
992
|
+
async create(dto) {
|
|
993
|
+
const order = await this.orderModel.create(dto)
|
|
994
|
+
this.eventEmitter.emit('order.created', { orderId: order._id }) // decoupled
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
### 4. No Global Exception Filter
|
|
1000
|
+
|
|
1001
|
+
```typescript
|
|
1002
|
+
// BAD — try/catch in every controller method
|
|
1003
|
+
@Get(':id')
|
|
1004
|
+
async findOne(@Param('id') id: string) {
|
|
1005
|
+
try {
|
|
1006
|
+
return await this.service.findOne(id)
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
throw new HttpException(error.message, 500)
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// GOOD — throw domain exceptions, global filter handles formatting
|
|
1013
|
+
@Get(':id')
|
|
1014
|
+
async findOne(@Param('id') id: string) {
|
|
1015
|
+
return this.service.findOneOrFail(id) // throws NotFoundException if missing
|
|
1016
|
+
}
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
### 5. Business Logic in Controllers
|
|
1020
|
+
|
|
1021
|
+
```typescript
|
|
1022
|
+
// BAD — controller does business logic
|
|
1023
|
+
@Post()
|
|
1024
|
+
async create(@Body() dto: CreateOrderDto) {
|
|
1025
|
+
const price = dto.items.reduce((sum, i) => sum + i.price * i.quantity, 0)
|
|
1026
|
+
const tax = price * 0.1
|
|
1027
|
+
const order = await this.orderModel.create({ ...dto, total: price + tax })
|
|
1028
|
+
await this.mailerService.send(...)
|
|
1029
|
+
return order
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// GOOD — controller delegates to service
|
|
1033
|
+
@Post()
|
|
1034
|
+
async create(@Body() dto: CreateOrderDto) {
|
|
1035
|
+
return this.orderService.create(dto)
|
|
1036
|
+
}
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
### 6. Missing Graceful Shutdown
|
|
1040
|
+
|
|
1041
|
+
```typescript
|
|
1042
|
+
// BAD — active queue jobs lost on deploy, connections leaked
|
|
1043
|
+
|
|
1044
|
+
// GOOD — clean shutdown
|
|
1045
|
+
app.enableShutdownHooks()
|
|
1046
|
+
|
|
1047
|
+
@Injectable()
|
|
1048
|
+
export class AppCleanupService implements OnApplicationShutdown {
|
|
1049
|
+
constructor(private queueService: BaseQueueService) {}
|
|
1050
|
+
|
|
1051
|
+
async onApplicationShutdown(signal: string) {
|
|
1052
|
+
logger.log(`Shutting down on ${signal}`)
|
|
1053
|
+
await this.queueService.gracefulShutdown(30_000)
|
|
1054
|
+
await mongoose.connection.close()
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
## Module Dependency Rules
|
|
1060
|
+
|
|
1061
|
+
1. **Core modules** are `@Global()` — imported once in `AppModule`, available everywhere
|
|
1062
|
+
2. **Feature modules** import only what they need — use `imports: [OtherModule]` and consume exported services
|
|
1063
|
+
3. **Shared utilities** are standalone — imported directly in features that need them
|
|
1064
|
+
4. **Circular dependencies** resolved with `forwardRef(() => ModuleA)` — keep these rare
|
|
1065
|
+
5. **Feature-to-feature communication** prefers events over direct imports
|
|
1066
|
+
6. **Never import a feature module just for a type** — move shared types to `shared/interfaces/`
|
|
1067
|
+
|
|
1068
|
+
## Quick Reference
|
|
1069
|
+
|
|
1070
|
+
| Task | Pattern |
|
|
1071
|
+
|------|---------|
|
|
1072
|
+
| New feature | Create `features/<name>/` with module, controller, service, dto/, entities/ |
|
|
1073
|
+
| Cross-feature side effect | `EventEmitter2` + `@OnEvent()` listener |
|
|
1074
|
+
| Async heavy work | Bull queue + processor |
|
|
1075
|
+
| Input validation | class-validator DTOs + global `ValidationPipe` |
|
|
1076
|
+
| Auth on route | `@Auth(Role.USER)` + `@CurrentUser()` |
|
|
1077
|
+
| Pagination | `@GetPagination()` decorator + `PaginatedResponse<T>` |
|
|
1078
|
+
| Error response | Throw `AppException` subclass, global filter formats it |
|
|
1079
|
+
| Cron job | `@Cron()` in `SchedulerService` |
|
|
1080
|
+
| Health check | Add indicator in `HealthController.check()` |
|
|
1081
|
+
| Config value | `ConfigService.get('KEY')` — validated at startup |
|
|
1082
|
+
|
|
1083
|
+
## Further Reading
|
|
1084
|
+
|
|
1085
|
+
For detailed reference on specific topics, see:
|
|
1086
|
+
- `references/advanced-patterns.md` — Middleware chains, custom decorators, interceptors, versioning, testing strategies, Docker setup, CI/CD patterns
|