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,590 @@
|
|
|
1
|
+
# Advanced NestJS Architecture Patterns
|
|
2
|
+
|
|
3
|
+
Detailed reference for patterns beyond the main SKILL.md. Load this when the user works on advanced infrastructure.
|
|
4
|
+
|
|
5
|
+
## Custom Decorators
|
|
6
|
+
|
|
7
|
+
### Compose Multiple Decorators
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// shared/decorators/auth.decorator.ts
|
|
11
|
+
import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common'
|
|
12
|
+
import { ApiBearerAuth, ApiUnauthorizedResponse } from '@nestjs/swagger'
|
|
13
|
+
|
|
14
|
+
export function Auth(...roles: Role[]) {
|
|
15
|
+
return applyDecorators(
|
|
16
|
+
SetMetadata('roles', roles),
|
|
17
|
+
UseGuards(HybridAuthGuard, RolesGuard),
|
|
18
|
+
ApiBearerAuth(),
|
|
19
|
+
ApiUnauthorizedResponse({ description: 'Unauthorized' }),
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Usage — single decorator replaces 4 lines
|
|
24
|
+
@Auth(Role.ADMIN)
|
|
25
|
+
@Get('admin/users')
|
|
26
|
+
async getAdminUsers() { ... }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Current User Decorator
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// shared/decorators/current-user.decorator.ts
|
|
33
|
+
export const CurrentUser = createParamDecorator(
|
|
34
|
+
(field: keyof UserDocument | undefined, ctx: ExecutionContext) => {
|
|
35
|
+
const request = ctx.switchToHttp().getRequest()
|
|
36
|
+
const user = request.user
|
|
37
|
+
return field ? user?.[field] : user
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// Usage
|
|
42
|
+
@Get('me')
|
|
43
|
+
async getProfile(@CurrentUser() user: UserDocument) { ... }
|
|
44
|
+
|
|
45
|
+
@Get('me/name')
|
|
46
|
+
async getName(@CurrentUser('firstName') name: string) { ... }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### API Field Selection Decorator
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// shared/decorators/api-select.decorator.ts
|
|
53
|
+
export const ApiSelect = createParamDecorator(
|
|
54
|
+
(data: unknown, ctx: ExecutionContext): string => {
|
|
55
|
+
const request = ctx.switchToHttp().getRequest()
|
|
56
|
+
return request.query.fields || '' // 'name,email,avatar'
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Usage in service
|
|
61
|
+
async findAll(fields: string) {
|
|
62
|
+
const select = fields.split(',').join(' ')
|
|
63
|
+
return this.userModel.find().select(select).lean()
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Interceptors
|
|
68
|
+
|
|
69
|
+
### Response Transform Interceptor
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// shared/interceptors/transform.interceptor.ts
|
|
73
|
+
@Injectable()
|
|
74
|
+
export class TransformInterceptor<T> implements NestInterceptor<T, ResponseWrapper<T>> {
|
|
75
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<ResponseWrapper<T>> {
|
|
76
|
+
return next.handle().pipe(
|
|
77
|
+
map((data) => ({
|
|
78
|
+
success: true,
|
|
79
|
+
data,
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
})),
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Logging Interceptor
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// shared/interceptors/logging.interceptor.ts
|
|
91
|
+
@Injectable()
|
|
92
|
+
export class LoggingInterceptor implements NestInterceptor {
|
|
93
|
+
private readonly logger = new Logger('HTTP')
|
|
94
|
+
|
|
95
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
96
|
+
const request = context.switchToHttp().getRequest()
|
|
97
|
+
const { method, url } = request
|
|
98
|
+
const start = Date.now()
|
|
99
|
+
|
|
100
|
+
return next.handle().pipe(
|
|
101
|
+
tap(() => {
|
|
102
|
+
const duration = Date.now() - start
|
|
103
|
+
this.logger.log(`${method} ${url} — ${duration}ms`)
|
|
104
|
+
}),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Cache Interceptor (Per-Route)
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// shared/interceptors/cache.interceptor.ts
|
|
114
|
+
@Injectable()
|
|
115
|
+
export class HttpCacheInterceptor extends CacheInterceptor {
|
|
116
|
+
trackBy(context: ExecutionContext): string | undefined {
|
|
117
|
+
const request = context.switchToHttp().getRequest()
|
|
118
|
+
// Only cache GET requests for authenticated users
|
|
119
|
+
if (request.method !== 'GET') return undefined
|
|
120
|
+
return `${request.user?.id}:${request.url}`
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Usage
|
|
125
|
+
@UseInterceptors(HttpCacheInterceptor)
|
|
126
|
+
@CacheTTL(300) // 5 minutes
|
|
127
|
+
@Get()
|
|
128
|
+
async findAll() { ... }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Middleware Patterns
|
|
132
|
+
|
|
133
|
+
### Request Context Middleware
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// shared/middleware/request-context.middleware.ts
|
|
137
|
+
@Injectable()
|
|
138
|
+
export class RequestContextMiddleware implements NestMiddleware {
|
|
139
|
+
use(req: Request, res: Response, next: NextFunction) {
|
|
140
|
+
// Attach unique request ID for tracing
|
|
141
|
+
req['requestId'] = req.headers['x-request-id'] || randomUUID()
|
|
142
|
+
res.setHeader('x-request-id', req['requestId'])
|
|
143
|
+
|
|
144
|
+
// Attach start time for duration tracking
|
|
145
|
+
req['startTime'] = Date.now()
|
|
146
|
+
|
|
147
|
+
next()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Register in module
|
|
152
|
+
export class AppModule implements NestModule {
|
|
153
|
+
configure(consumer: MiddlewareConsumer) {
|
|
154
|
+
consumer.apply(RequestContextMiddleware).forRoutes('*')
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Conditional Middleware
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// Apply middleware only to specific routes
|
|
163
|
+
export class AppModule implements NestModule {
|
|
164
|
+
configure(consumer: MiddlewareConsumer) {
|
|
165
|
+
consumer
|
|
166
|
+
.apply(RawBodyMiddleware)
|
|
167
|
+
.forRoutes({ path: 'webhooks/*', method: RequestMethod.POST })
|
|
168
|
+
|
|
169
|
+
consumer
|
|
170
|
+
.apply(LoggingMiddleware)
|
|
171
|
+
.exclude({ path: 'health', method: RequestMethod.GET })
|
|
172
|
+
.forRoutes('*')
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## API Versioning
|
|
178
|
+
|
|
179
|
+
### URI Versioning (Recommended)
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// main.ts
|
|
183
|
+
app.enableVersioning({
|
|
184
|
+
type: VersioningType.URI,
|
|
185
|
+
defaultVersion: '1',
|
|
186
|
+
prefix: 'api/v',
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// Controller
|
|
190
|
+
@Controller({ path: 'users', version: '1' })
|
|
191
|
+
export class UserV1Controller { ... }
|
|
192
|
+
|
|
193
|
+
@Controller({ path: 'users', version: '2' })
|
|
194
|
+
export class UserV2Controller { ... }
|
|
195
|
+
|
|
196
|
+
// Routes:
|
|
197
|
+
// GET /api/v1/users → UserV1Controller
|
|
198
|
+
// GET /api/v2/users → UserV2Controller
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Version-Neutral Routes
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Health check available at /health (no version prefix)
|
|
205
|
+
@Controller({ path: 'health', version: VERSION_NEUTRAL })
|
|
206
|
+
export class HealthController { ... }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Testing Strategies
|
|
210
|
+
|
|
211
|
+
### Unit Test — Service
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
// features/order/order.service.spec.ts
|
|
215
|
+
describe('OrderService', () => {
|
|
216
|
+
let service: OrderService
|
|
217
|
+
let model: Model<OrderDocument>
|
|
218
|
+
|
|
219
|
+
beforeEach(async () => {
|
|
220
|
+
const module = await Test.createTestingModule({
|
|
221
|
+
providers: [
|
|
222
|
+
OrderService,
|
|
223
|
+
{
|
|
224
|
+
provide: getModelToken(Order.name),
|
|
225
|
+
useValue: {
|
|
226
|
+
find: jest.fn(),
|
|
227
|
+
findOne: jest.fn(),
|
|
228
|
+
create: jest.fn(),
|
|
229
|
+
countDocuments: jest.fn(),
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
provide: EventEmitter2,
|
|
234
|
+
useValue: { emit: jest.fn() },
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
}).compile()
|
|
238
|
+
|
|
239
|
+
service = module.get(OrderService)
|
|
240
|
+
model = module.get(getModelToken(Order.name))
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
describe('findOneOrFail', () => {
|
|
244
|
+
it('should throw NotFoundException when order not found', async () => {
|
|
245
|
+
jest.spyOn(model, 'findOne').mockReturnValue({
|
|
246
|
+
lean: () => ({ exec: () => Promise.resolve(null) }),
|
|
247
|
+
} as any)
|
|
248
|
+
|
|
249
|
+
await expect(service.findOneOrFail('id', 'userId'))
|
|
250
|
+
.rejects.toThrow(NotFoundException)
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Integration Test — Controller
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// features/order/order.controller.spec.ts
|
|
260
|
+
describe('OrderController (e2e)', () => {
|
|
261
|
+
let app: INestApplication
|
|
262
|
+
let mongoServer: MongoMemoryServer
|
|
263
|
+
|
|
264
|
+
beforeAll(async () => {
|
|
265
|
+
mongoServer = await MongoMemoryServer.create()
|
|
266
|
+
|
|
267
|
+
const module = await Test.createTestingModule({
|
|
268
|
+
imports: [
|
|
269
|
+
MongooseModule.forRoot(mongoServer.getUri()),
|
|
270
|
+
OrderModule,
|
|
271
|
+
],
|
|
272
|
+
}).compile()
|
|
273
|
+
|
|
274
|
+
app = module.createNestApplication()
|
|
275
|
+
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }))
|
|
276
|
+
await app.init()
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
afterAll(async () => {
|
|
280
|
+
await app.close()
|
|
281
|
+
await mongoServer.stop()
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('POST /orders — should validate input', () => {
|
|
285
|
+
return request(app.getHttpServer())
|
|
286
|
+
.post('/orders')
|
|
287
|
+
.send({ items: [] }) // empty items should fail
|
|
288
|
+
.expect(422)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it('POST /orders — should create order', () => {
|
|
292
|
+
return request(app.getHttpServer())
|
|
293
|
+
.post('/orders')
|
|
294
|
+
.send({ items: [{ productId: 'abc', quantity: 2 }] })
|
|
295
|
+
.expect(201)
|
|
296
|
+
.expect((res) => {
|
|
297
|
+
expect(res.body.total).toBeDefined()
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Testing Queue Processors
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
describe('OrderFulfillmentProcessor', () => {
|
|
307
|
+
let processor: OrderFulfillmentProcessor
|
|
308
|
+
|
|
309
|
+
beforeEach(async () => {
|
|
310
|
+
const module = await Test.createTestingModule({
|
|
311
|
+
providers: [
|
|
312
|
+
OrderFulfillmentProcessor,
|
|
313
|
+
{ provide: OrderService, useValue: { findById: jest.fn(), updateStatus: jest.fn() } },
|
|
314
|
+
{ provide: NotificationService, useValue: { send: jest.fn() } },
|
|
315
|
+
],
|
|
316
|
+
}).compile()
|
|
317
|
+
|
|
318
|
+
processor = module.get(OrderFulfillmentProcessor)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('should process fulfillment job', async () => {
|
|
322
|
+
const mockJob = { data: { orderId: '123' }, progress: jest.fn() } as any
|
|
323
|
+
const result = await processor.handleFulfillment(mockJob)
|
|
324
|
+
expect(result.status).toBe('confirmed')
|
|
325
|
+
expect(mockJob.progress).toHaveBeenCalledWith(100)
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Docker Setup
|
|
331
|
+
|
|
332
|
+
### Multi-Stage Dockerfile
|
|
333
|
+
|
|
334
|
+
```dockerfile
|
|
335
|
+
# Stage 1: Build
|
|
336
|
+
FROM node:20-alpine AS builder
|
|
337
|
+
WORKDIR /app
|
|
338
|
+
COPY package.json package-lock.json ./
|
|
339
|
+
RUN npm ci --ignore-scripts
|
|
340
|
+
COPY . .
|
|
341
|
+
RUN npm run build
|
|
342
|
+
RUN npm prune --production
|
|
343
|
+
|
|
344
|
+
# Stage 2: Production
|
|
345
|
+
FROM node:20-alpine AS production
|
|
346
|
+
WORKDIR /app
|
|
347
|
+
|
|
348
|
+
RUN addgroup -g 1001 -S nestjs && adduser -S nestjs -u 1001
|
|
349
|
+
COPY --from=builder --chown=nestjs:nestjs /app/dist ./dist
|
|
350
|
+
COPY --from=builder --chown=nestjs:nestjs /app/node_modules ./node_modules
|
|
351
|
+
COPY --from=builder --chown=nestjs:nestjs /app/package.json ./
|
|
352
|
+
|
|
353
|
+
USER nestjs
|
|
354
|
+
EXPOSE 3000
|
|
355
|
+
CMD ["node", "dist/main.js"]
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Docker Compose (Development)
|
|
359
|
+
|
|
360
|
+
```yaml
|
|
361
|
+
version: '3.8'
|
|
362
|
+
services:
|
|
363
|
+
api:
|
|
364
|
+
build: .
|
|
365
|
+
ports: ['3000:3000']
|
|
366
|
+
env_file: .env
|
|
367
|
+
depends_on:
|
|
368
|
+
mongodb:
|
|
369
|
+
condition: service_healthy
|
|
370
|
+
redis:
|
|
371
|
+
condition: service_healthy
|
|
372
|
+
volumes:
|
|
373
|
+
- ./uploads:/app/uploads
|
|
374
|
+
|
|
375
|
+
mongodb:
|
|
376
|
+
image: mongo:7
|
|
377
|
+
ports: ['27017:27017']
|
|
378
|
+
volumes: ['mongo-data:/data/db']
|
|
379
|
+
healthcheck:
|
|
380
|
+
test: mongosh --eval "db.adminCommand('ping')"
|
|
381
|
+
interval: 10s
|
|
382
|
+
timeout: 5s
|
|
383
|
+
retries: 3
|
|
384
|
+
|
|
385
|
+
redis:
|
|
386
|
+
image: redis:7-alpine
|
|
387
|
+
ports: ['6379:6379']
|
|
388
|
+
healthcheck:
|
|
389
|
+
test: redis-cli ping
|
|
390
|
+
interval: 10s
|
|
391
|
+
timeout: 5s
|
|
392
|
+
retries: 3
|
|
393
|
+
|
|
394
|
+
volumes:
|
|
395
|
+
mongo-data:
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Swagger / OpenAPI Setup
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// core/config/swagger.config.ts
|
|
402
|
+
export function setupSwagger(app: INestApplication) {
|
|
403
|
+
const config = new DocumentBuilder()
|
|
404
|
+
.setTitle('API Documentation')
|
|
405
|
+
.setVersion('1.0')
|
|
406
|
+
.addBearerAuth({
|
|
407
|
+
type: 'http',
|
|
408
|
+
scheme: 'bearer',
|
|
409
|
+
bearerFormat: 'JWT',
|
|
410
|
+
})
|
|
411
|
+
.build()
|
|
412
|
+
|
|
413
|
+
const document = SwaggerModule.createDocument(app, config)
|
|
414
|
+
SwaggerModule.setup('docs', app, document, {
|
|
415
|
+
swaggerOptions: {
|
|
416
|
+
persistAuthorization: true,
|
|
417
|
+
tagsSorter: 'alpha',
|
|
418
|
+
operationsSorter: 'alpha',
|
|
419
|
+
},
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Swagger Decorators on DTOs
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
export class CreateUserDto {
|
|
428
|
+
@ApiProperty({ example: 'John', description: 'First name' })
|
|
429
|
+
@IsString()
|
|
430
|
+
@MinLength(2)
|
|
431
|
+
firstName: string
|
|
432
|
+
|
|
433
|
+
@ApiProperty({ example: 'john@example.com' })
|
|
434
|
+
@IsEmail()
|
|
435
|
+
email: string
|
|
436
|
+
|
|
437
|
+
@ApiPropertyOptional({ example: '+1234567890' })
|
|
438
|
+
@IsString()
|
|
439
|
+
@IsOptional()
|
|
440
|
+
phone?: string
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## File Upload Pattern
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
// features/media/media.controller.ts
|
|
448
|
+
@Controller('media')
|
|
449
|
+
export class MediaController {
|
|
450
|
+
constructor(private readonly mediaService: MediaStorageService) {}
|
|
451
|
+
|
|
452
|
+
@Post('upload')
|
|
453
|
+
@Auth(Role.USER)
|
|
454
|
+
@UseInterceptors(FileInterceptor('file', {
|
|
455
|
+
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
|
456
|
+
fileFilter: (req, file, cb) => {
|
|
457
|
+
const allowed = /\.(jpg|jpeg|png|webp|gif)$/i
|
|
458
|
+
if (!allowed.test(file.originalname)) {
|
|
459
|
+
return cb(new BadRequestException('Only image files allowed'), false)
|
|
460
|
+
}
|
|
461
|
+
cb(null, true)
|
|
462
|
+
},
|
|
463
|
+
}))
|
|
464
|
+
async upload(
|
|
465
|
+
@CurrentUser() user: UserDocument,
|
|
466
|
+
@UploadedFile() file: Express.Multer.File,
|
|
467
|
+
) {
|
|
468
|
+
return this.mediaService.upload(file, { userId: user.id })
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Storage Abstraction (S3 / Local)
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
// features/media/services/media-storage.service.ts
|
|
477
|
+
@Injectable()
|
|
478
|
+
export class MediaStorageService {
|
|
479
|
+
private readonly strategy: StorageStrategy
|
|
480
|
+
|
|
481
|
+
constructor(private config: ConfigService) {
|
|
482
|
+
this.strategy = config.get('STORAGE_TYPE') === 's3'
|
|
483
|
+
? new S3StorageStrategy(config)
|
|
484
|
+
: new LocalStorageStrategy(config)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async upload(file: Express.Multer.File, opts: UploadOpts): Promise<UploadResult> {
|
|
488
|
+
return this.strategy.upload(file, opts)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async delete(key: string): Promise<void> {
|
|
492
|
+
return this.strategy.delete(key)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
## WebSocket Gateway
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
// features/chat/chat.gateway.ts
|
|
501
|
+
@WebSocketGateway({
|
|
502
|
+
cors: { origin: '*' },
|
|
503
|
+
namespace: '/chat',
|
|
504
|
+
})
|
|
505
|
+
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|
506
|
+
@WebSocketServer()
|
|
507
|
+
server: Server
|
|
508
|
+
|
|
509
|
+
private readonly logger = new Logger('ChatGateway')
|
|
510
|
+
|
|
511
|
+
handleConnection(client: Socket) {
|
|
512
|
+
this.logger.log(`Client connected: ${client.id}`)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
handleDisconnect(client: Socket) {
|
|
516
|
+
this.logger.log(`Client disconnected: ${client.id}`)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
@SubscribeMessage('message')
|
|
520
|
+
handleMessage(client: Socket, payload: { room: string; content: string }) {
|
|
521
|
+
this.server.to(payload.room).emit('message', {
|
|
522
|
+
sender: client.id,
|
|
523
|
+
content: payload.content,
|
|
524
|
+
timestamp: new Date(),
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
## CI/CD Patterns
|
|
531
|
+
|
|
532
|
+
### Git Hooks (Husky + lint-staged)
|
|
533
|
+
|
|
534
|
+
```json
|
|
535
|
+
// package.json
|
|
536
|
+
{
|
|
537
|
+
"lint-staged": {
|
|
538
|
+
"*.ts": ["eslint --fix", "prettier --write"]
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
# .husky/pre-commit
|
|
545
|
+
npx lint-staged
|
|
546
|
+
|
|
547
|
+
# .husky/commit-msg
|
|
548
|
+
npx commitlint --edit $1
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Commitlint Config
|
|
552
|
+
|
|
553
|
+
```javascript
|
|
554
|
+
// commitlint.config.js
|
|
555
|
+
module.exports = {
|
|
556
|
+
extends: ['@commitlint/config-conventional'],
|
|
557
|
+
rules: {
|
|
558
|
+
'type-enum': [2, 'always', [
|
|
559
|
+
'feat', 'fix', 'docs', 'style', 'refactor',
|
|
560
|
+
'perf', 'test', 'build', 'ci', 'chore', 'revert',
|
|
561
|
+
]],
|
|
562
|
+
'subject-max-length': [2, 'always', 100],
|
|
563
|
+
},
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## Scaling Considerations
|
|
568
|
+
|
|
569
|
+
### Horizontal Scaling Checklist
|
|
570
|
+
|
|
571
|
+
- [ ] API layer is stateless (no in-memory sessions)
|
|
572
|
+
- [ ] Session/auth state stored in Redis
|
|
573
|
+
- [ ] File uploads go to S3 (not local disk)
|
|
574
|
+
- [ ] Queue workers can run as separate processes
|
|
575
|
+
- [ ] Database connection pool sized per instance
|
|
576
|
+
- [ ] Health checks return instance-specific metrics
|
|
577
|
+
- [ ] Graceful shutdown handles SIGTERM
|
|
578
|
+
- [ ] Sticky sessions disabled (or WebSocket uses Redis adapter)
|
|
579
|
+
|
|
580
|
+
### Extracting Microservices
|
|
581
|
+
|
|
582
|
+
When a feature outgrows the monolith:
|
|
583
|
+
|
|
584
|
+
1. The feature already has its own module with clear boundaries
|
|
585
|
+
2. Extract the module into a standalone NestJS app
|
|
586
|
+
3. Replace direct imports with HTTP/gRPC/message queue calls
|
|
587
|
+
4. Events already decouple side effects — minimal rewiring needed
|
|
588
|
+
5. Shared DTOs/interfaces move to a common package
|
|
589
|
+
|
|
590
|
+
This is the strength of feature-based architecture: each module is already a microservice boundary waiting to be extracted.
|