@jsfsi-core/ts-nestjs 1.1.5 → 1.1.8
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 +628 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
# @jsfsi-core/ts-nestjs
|
|
2
|
+
|
|
3
|
+
NestJS-specific utilities for building robust backend applications following hexagonal architecture and domain-driven design principles.
|
|
4
|
+
|
|
5
|
+
## 📦 Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @jsfsi-core/ts-nestjs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Peer Dependencies:**
|
|
12
|
+
|
|
13
|
+
- `@nestjs/core`
|
|
14
|
+
- `@nestjs/common`
|
|
15
|
+
- `express`
|
|
16
|
+
- `body-parser`
|
|
17
|
+
|
|
18
|
+
## 🏗️ Architecture
|
|
19
|
+
|
|
20
|
+
This package provides NestJS-specific implementations of hexagonal architecture patterns:
|
|
21
|
+
|
|
22
|
+
- **Application Bootstrap**: Configured NestJS application factory
|
|
23
|
+
- **Configuration**: Type-safe configuration service with Zod validation
|
|
24
|
+
- **Exception Filters**: Centralized error handling at application edges
|
|
25
|
+
- **Validators**: Type-safe request validation decorators
|
|
26
|
+
- **Middlewares**: Request logging and common middleware
|
|
27
|
+
|
|
28
|
+
### Application Structure
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
src/
|
|
32
|
+
├── app/
|
|
33
|
+
│ ├── app.ts # Application factory
|
|
34
|
+
│ └── bootstrap.ts # Bootstrap helper
|
|
35
|
+
├── configuration/
|
|
36
|
+
│ └── AppConfigurationService.ts # Configuration setup
|
|
37
|
+
├── filters/
|
|
38
|
+
│ └── AllExceptionsFilter.ts # Exception handler (edge)
|
|
39
|
+
├── middlewares/
|
|
40
|
+
│ └── RequestMiddleware.ts # Request logging
|
|
41
|
+
└── validators/
|
|
42
|
+
└── ZodValidator.ts # Request validators
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 📋 Features
|
|
46
|
+
|
|
47
|
+
### Application Bootstrap
|
|
48
|
+
|
|
49
|
+
Type-safe application creation with pre-configured settings:
|
|
50
|
+
|
|
51
|
+
**main.ts:**
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import 'reflect-metadata';
|
|
55
|
+
|
|
56
|
+
import * as path from 'path';
|
|
57
|
+
import { bootstrap } from '@jsfsi-core/ts-nestjs';
|
|
58
|
+
|
|
59
|
+
import { AppModule } from './app/AppModule';
|
|
60
|
+
|
|
61
|
+
bootstrap({
|
|
62
|
+
appModule: AppModule,
|
|
63
|
+
configPath: path.resolve(__dirname, '../configuration'),
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The `bootstrap` function:
|
|
68
|
+
|
|
69
|
+
- Loads environment configuration from the specified `configPath`
|
|
70
|
+
- Creates and configures the NestJS application
|
|
71
|
+
- Automatically starts the application on the port specified in your configuration
|
|
72
|
+
- Handles CORS, exception filters, and logging setup
|
|
73
|
+
|
|
74
|
+
### Configuration Service
|
|
75
|
+
|
|
76
|
+
Type-safe configuration with Zod schemas:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { z } from 'zod';
|
|
80
|
+
import { AppConfigSchema, appConfigModuleSetup, APP_CONFIG_TOKEN } from '@jsfsi-core/ts-nestjs';
|
|
81
|
+
import { ConfigService } from '@nestjs/config';
|
|
82
|
+
|
|
83
|
+
// Define configuration schema
|
|
84
|
+
export const AppConfigSchema = z.object({
|
|
85
|
+
APP_PORT: z
|
|
86
|
+
.string()
|
|
87
|
+
.transform((val) => parseInt(val, 10))
|
|
88
|
+
.refine((val) => !isNaN(val), { message: 'APP_PORT must be a valid number' }),
|
|
89
|
+
DATABASE_URL: z.string().url(),
|
|
90
|
+
CORS_ORIGIN: z.string().default('*'),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export type AppConfig = z.infer<typeof AppConfigSchema>;
|
|
94
|
+
|
|
95
|
+
// In your app module (AppModule.ts)
|
|
96
|
+
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
|
|
97
|
+
import { appConfigModuleSetup, RequestMiddleware } from '@jsfsi-core/ts-nestjs';
|
|
98
|
+
|
|
99
|
+
import { BrowserAdapter } from '../adapters/BrowserAdapter';
|
|
100
|
+
import { HealthController } from '../communication/controllers/health/HealthController';
|
|
101
|
+
import { RenderController } from '../communication/controllers/render/RenderController';
|
|
102
|
+
import { RenderService } from '../domain/RenderService';
|
|
103
|
+
|
|
104
|
+
const controllers = [HealthController, RenderController];
|
|
105
|
+
const services = [RenderService];
|
|
106
|
+
const adapters = [BrowserAdapter];
|
|
107
|
+
|
|
108
|
+
@Module({
|
|
109
|
+
imports: [appConfigModuleSetup()],
|
|
110
|
+
controllers: [...controllers],
|
|
111
|
+
providers: [...services, ...adapters],
|
|
112
|
+
})
|
|
113
|
+
export class AppModule implements NestModule {
|
|
114
|
+
configure(consumer: MiddlewareConsumer): void {
|
|
115
|
+
consumer.apply(RequestMiddleware).forRoutes('*');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Use in service
|
|
120
|
+
@Injectable()
|
|
121
|
+
export class MyService {
|
|
122
|
+
constructor(private readonly configService: ConfigService) {}
|
|
123
|
+
|
|
124
|
+
someMethod() {
|
|
125
|
+
const config = this.configService.get<AppConfig>(APP_CONFIG_TOKEN);
|
|
126
|
+
// config is fully typed
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Exception Filter
|
|
132
|
+
|
|
133
|
+
Centralized exception handling at the application edge:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { AllExceptionsFilter } from '@jsfsi-core/ts-nestjs';
|
|
137
|
+
import { HttpAdapterHost } from '@nestjs/core';
|
|
138
|
+
|
|
139
|
+
// Automatically registered in createApp()
|
|
140
|
+
// Or manually:
|
|
141
|
+
app.useGlobalFilters(new AllExceptionsFilter(httpAdapterHost));
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
This filter:
|
|
145
|
+
|
|
146
|
+
- Catches all unhandled exceptions
|
|
147
|
+
- Maps HTTP exceptions to appropriate status codes
|
|
148
|
+
- Logs errors for monitoring
|
|
149
|
+
- Returns consistent error responses
|
|
150
|
+
|
|
151
|
+
**Note**: This is where exceptions are caught (edge of hexagonal architecture).
|
|
152
|
+
|
|
153
|
+
### Request Validation
|
|
154
|
+
|
|
155
|
+
Type-safe request validation with Zod:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
import { Controller, Post } from '@nestjs/common';
|
|
159
|
+
import { SafeBody, SafeQuery, SafeParams } from '@jsfsi-core/ts-nestjs';
|
|
160
|
+
import { z } from 'zod';
|
|
161
|
+
|
|
162
|
+
const CreateUserSchema = z.object({
|
|
163
|
+
email: z.string().email(),
|
|
164
|
+
name: z.string().min(1),
|
|
165
|
+
age: z.number().int().positive(),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
@Controller('users')
|
|
169
|
+
export class UserController {
|
|
170
|
+
@Post()
|
|
171
|
+
async createUser(@SafeBody(CreateUserSchema) user: z.infer<typeof CreateUserSchema>) {
|
|
172
|
+
// user is fully typed based on schema
|
|
173
|
+
// Validation happens automatically
|
|
174
|
+
// Returns 400 Bad Request if validation fails
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@Get(':id')
|
|
178
|
+
async getUser(@SafeParams(z.object({ id: z.string().uuid() })) params: { id: string }) {
|
|
179
|
+
// params.id is validated as UUID
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@Get()
|
|
183
|
+
async listUsers(
|
|
184
|
+
@SafeQuery(z.object({ page: z.string().transform(Number).optional() }))
|
|
185
|
+
query: {
|
|
186
|
+
page?: number;
|
|
187
|
+
},
|
|
188
|
+
) {
|
|
189
|
+
// query.page is validated and transformed
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Request Middleware
|
|
195
|
+
|
|
196
|
+
Automatic request logging:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
import { RequestMiddleware } from '@jsfsi-core/ts-nestjs';
|
|
200
|
+
|
|
201
|
+
// In your app module
|
|
202
|
+
@Module({
|
|
203
|
+
// ...
|
|
204
|
+
})
|
|
205
|
+
export class AppModule implements NestModule {
|
|
206
|
+
configure(consumer: MiddlewareConsumer) {
|
|
207
|
+
consumer.apply(RequestMiddleware).forRoutes('*');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Logs include:
|
|
213
|
+
|
|
214
|
+
- HTTP method and URL
|
|
215
|
+
- Status code
|
|
216
|
+
- Response time
|
|
217
|
+
- Request/response headers
|
|
218
|
+
- Severity level based on status code
|
|
219
|
+
|
|
220
|
+
## 📝 Naming Conventions
|
|
221
|
+
|
|
222
|
+
### Controllers
|
|
223
|
+
|
|
224
|
+
- **Controllers**: PascalCase suffix with `Controller` (e.g., `UserController`, `AuthController`)
|
|
225
|
+
- **Endpoints**: Use RESTful naming (e.g., `getUser`, `createUser`, `updateUser`)
|
|
226
|
+
|
|
227
|
+
### Services
|
|
228
|
+
|
|
229
|
+
- **Services**: PascalCase suffix with `Service` (e.g., `UserService`, `AuthService`)
|
|
230
|
+
- **Domain Services**: Live in domain layer, not in NestJS services
|
|
231
|
+
|
|
232
|
+
### Modules
|
|
233
|
+
|
|
234
|
+
- **Modules**: PascalCase suffix with `Module` (e.g., `UserModule`, `AppModule`)
|
|
235
|
+
|
|
236
|
+
## 🧪 Testing Principles
|
|
237
|
+
|
|
238
|
+
### Testing Controllers
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { TestingApp } from '@jsfsi-core/ts-nestjs';
|
|
242
|
+
import { Controller, Get } from '@nestjs/common';
|
|
243
|
+
|
|
244
|
+
@Controller('test')
|
|
245
|
+
class TestController {
|
|
246
|
+
@Get()
|
|
247
|
+
getHello(): { message: string } {
|
|
248
|
+
return { message: 'Hello' };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
describe('TestController', () => {
|
|
253
|
+
it('returns hello message', async () => {
|
|
254
|
+
const app = await TestingApp.create({
|
|
255
|
+
controllers: [TestController],
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const response = await app.get('/test');
|
|
259
|
+
|
|
260
|
+
expect(response.status).toBe(200);
|
|
261
|
+
expect(response.body).toEqual({ message: 'Hello' });
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Testing Services
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
import { Test } from '@nestjs/testing';
|
|
270
|
+
import { ConfigService } from '@nestjs/config';
|
|
271
|
+
|
|
272
|
+
describe('UserService', () => {
|
|
273
|
+
let service: UserService;
|
|
274
|
+
|
|
275
|
+
beforeEach(async () => {
|
|
276
|
+
const module = await Test.createTestingModule({
|
|
277
|
+
providers: [
|
|
278
|
+
UserService,
|
|
279
|
+
{
|
|
280
|
+
provide: ConfigService,
|
|
281
|
+
useValue: {
|
|
282
|
+
get: jest.fn(),
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
}).compile();
|
|
287
|
+
|
|
288
|
+
service = module.get<UserService>(UserService);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should be defined', () => {
|
|
292
|
+
expect(service).toBeDefined();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Testing with Result Types
|
|
298
|
+
|
|
299
|
+
When services return Result types, test accordingly:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { isFailure } from '@jsfsi-core/ts-crossplatform';
|
|
303
|
+
|
|
304
|
+
describe('AuthService', () => {
|
|
305
|
+
it('returns user on successful sign in', async () => {
|
|
306
|
+
const [user, failure] = await authService.signIn(email, password);
|
|
307
|
+
|
|
308
|
+
expect(user).toBeDefined();
|
|
309
|
+
expect(failure).toBeUndefined();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('returns SignInFailure on authentication error', async () => {
|
|
313
|
+
const [user, failure] = await authService.signIn(email, password);
|
|
314
|
+
|
|
315
|
+
expect(user).toBeUndefined();
|
|
316
|
+
expect(isFailure(SignInFailure)(failure)).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
## ⚠️ Error Handling Principles
|
|
322
|
+
|
|
323
|
+
### Exception Filter at Edge
|
|
324
|
+
|
|
325
|
+
**Exceptions should only be thrown at the edge** (in controllers/exception filters), not in domain logic:
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
// ✅ Good - In controller (edge)
|
|
329
|
+
@Controller('auth')
|
|
330
|
+
export class AuthController {
|
|
331
|
+
constructor(private readonly authService: AuthenticationService) {}
|
|
332
|
+
|
|
333
|
+
@Post('signin')
|
|
334
|
+
async signIn(@SafeBody(SignInSchema) body: SignInDto) {
|
|
335
|
+
const [user, failure] = await this.authService.signIn(body.email, body.password);
|
|
336
|
+
|
|
337
|
+
if (isFailure(SignInFailure)(failure)) {
|
|
338
|
+
throw new UnauthorizedException('Invalid credentials');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return user;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ✅ Good - Domain service returns Result
|
|
346
|
+
export class AuthenticationService {
|
|
347
|
+
async signIn(email: string, password: string): Promise<Result<User, SignInFailure>> {
|
|
348
|
+
// No exceptions thrown here
|
|
349
|
+
return this.authAdapter.signIn(email, password);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ✅ Good - Exception filter catches all exceptions
|
|
354
|
+
@Catch()
|
|
355
|
+
export class AllExceptionsFilter implements ExceptionFilter {
|
|
356
|
+
catch(error: unknown, host: ArgumentsHost) {
|
|
357
|
+
// All exceptions caught here (edge)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ❌ Bad - Throwing in domain service
|
|
362
|
+
export class AuthenticationService {
|
|
363
|
+
async signIn(email: string, password: string): Promise<User> {
|
|
364
|
+
// Don't throw exceptions in domain layer
|
|
365
|
+
if (!isValid(email)) {
|
|
366
|
+
throw new Error('Invalid email');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Result Types in Domain
|
|
373
|
+
|
|
374
|
+
Domain services should return `Result` types:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// ✅ Good
|
|
378
|
+
@Injectable()
|
|
379
|
+
export class UserService {
|
|
380
|
+
async getUser(id: string): Promise<Result<User, UserNotFoundFailure>> {
|
|
381
|
+
const [user, failure] = await this.userRepository.findById(id);
|
|
382
|
+
|
|
383
|
+
if (isFailure(UserNotFoundFailure)(failure)) {
|
|
384
|
+
return Fail(failure);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return Ok(user);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ✅ Good - Mapping Result to HTTP in controller
|
|
392
|
+
@Controller('users')
|
|
393
|
+
export class UserController {
|
|
394
|
+
@Get(':id')
|
|
395
|
+
async getUser(@SafeParams(IdSchema) params: { id: string }) {
|
|
396
|
+
const [user, failure] = await this.userService.getUser(params.id);
|
|
397
|
+
|
|
398
|
+
if (isFailure(UserNotFoundFailure)(failure)) {
|
|
399
|
+
throw new NotFoundException('User not found');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return user;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Validation Errors
|
|
408
|
+
|
|
409
|
+
Use `SafeBody`, `SafeQuery`, `SafeParams` for automatic validation:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// ✅ Good - Automatic validation
|
|
413
|
+
@Post('users')
|
|
414
|
+
async createUser(@SafeBody(CreateUserSchema) user: CreateUserDto) {
|
|
415
|
+
// user is already validated
|
|
416
|
+
return this.userService.create(user);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// ❌ Bad - Manual validation
|
|
420
|
+
@Post('users')
|
|
421
|
+
async createUser(@Body() user: any) {
|
|
422
|
+
// Manual validation needed
|
|
423
|
+
if (!user.email) {
|
|
424
|
+
throw new BadRequestException('Email required');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## 🎯 Domain-Driven Design
|
|
430
|
+
|
|
431
|
+
### Domain Layer Structure
|
|
432
|
+
|
|
433
|
+
Domain logic should be framework-agnostic:
|
|
434
|
+
|
|
435
|
+
```
|
|
436
|
+
src/
|
|
437
|
+
├── domain/
|
|
438
|
+
│ ├── models/
|
|
439
|
+
│ │ ├── User.ts
|
|
440
|
+
│ │ └── SignInFailure.ts
|
|
441
|
+
│ └── services/
|
|
442
|
+
│ └── AuthenticationService.ts # Domain service (no NestJS dependencies)
|
|
443
|
+
├── adapters/
|
|
444
|
+
│ └── DatabaseAdapter.ts # Implements domain interfaces
|
|
445
|
+
└── controllers/ # NestJS-specific (edge)
|
|
446
|
+
└── AuthController.ts
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Domain Services
|
|
450
|
+
|
|
451
|
+
Domain services contain business logic:
|
|
452
|
+
|
|
453
|
+
```typescript
|
|
454
|
+
// ✅ Good - Domain service (no NestJS decorators)
|
|
455
|
+
export class AuthenticationService {
|
|
456
|
+
constructor(private readonly authAdapter: AuthenticationAdapter) {}
|
|
457
|
+
|
|
458
|
+
async signIn(email: string, password: string): Promise<Result<User, SignInFailure>> {
|
|
459
|
+
// Business logic here
|
|
460
|
+
return this.authAdapter.signIn(email, password);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ✅ Good - Inject domain service in NestJS service
|
|
465
|
+
@Injectable()
|
|
466
|
+
export class AuthService {
|
|
467
|
+
constructor(private readonly authenticationService: AuthenticationService) {}
|
|
468
|
+
|
|
469
|
+
async signIn(email: string, password: string) {
|
|
470
|
+
return this.authenticationService.signIn(email, password);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## 🔄 Result Class Integration
|
|
476
|
+
|
|
477
|
+
### Using Result Types
|
|
478
|
+
|
|
479
|
+
Domain services return Result types, controllers map to HTTP:
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
import { Result, isFailure } from '@jsfsi-core/ts-crossplatform';
|
|
483
|
+
|
|
484
|
+
@Controller('orders')
|
|
485
|
+
export class OrderController {
|
|
486
|
+
constructor(private readonly orderService: OrderService) {}
|
|
487
|
+
|
|
488
|
+
@Post()
|
|
489
|
+
async createOrder(@SafeBody(CreateOrderSchema) order: CreateOrderDto) {
|
|
490
|
+
const [orderId, failure] = await this.orderService.create(order);
|
|
491
|
+
|
|
492
|
+
if (isFailure(ValidationFailure)(failure)) {
|
|
493
|
+
throw new BadRequestException(failure.message);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (isFailure(PaymentFailure)(failure)) {
|
|
497
|
+
throw new PaymentRequiredException('Payment failed');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return { id: orderId };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Error Mapping
|
|
506
|
+
|
|
507
|
+
Map domain failures to HTTP exceptions:
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
function mapFailureToHttpException(failure: Failure): HttpException {
|
|
511
|
+
if (isFailure(ValidationFailure)(failure)) {
|
|
512
|
+
return new BadRequestException(failure.message);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (isFailure(NotFoundFailure)(failure)) {
|
|
516
|
+
return new NotFoundException(failure.message);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (isFailure(UnauthorizedFailure)(failure)) {
|
|
520
|
+
return new UnauthorizedException(failure.message);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return new InternalServerErrorException('An error occurred');
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
## 📚 Best Practices
|
|
528
|
+
|
|
529
|
+
### 1. Dependency Injection
|
|
530
|
+
|
|
531
|
+
Use constructor injection:
|
|
532
|
+
|
|
533
|
+
```typescript
|
|
534
|
+
@Injectable()
|
|
535
|
+
export class UserService {
|
|
536
|
+
constructor(
|
|
537
|
+
private readonly userRepository: UserRepository,
|
|
538
|
+
private readonly configService: ConfigService,
|
|
539
|
+
) {}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### 2. Module Organization
|
|
544
|
+
|
|
545
|
+
Group related functionality in modules:
|
|
546
|
+
|
|
547
|
+
```typescript
|
|
548
|
+
@Module({
|
|
549
|
+
imports: [TypeOrmModule.forFeature([UserEntity])],
|
|
550
|
+
controllers: [UserController],
|
|
551
|
+
providers: [UserService, UserRepository],
|
|
552
|
+
exports: [UserService],
|
|
553
|
+
})
|
|
554
|
+
export class UserModule {}
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### 3. Configuration
|
|
558
|
+
|
|
559
|
+
Always use typed configuration:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// ✅ Good
|
|
563
|
+
const config = this.configService.get<AppConfig>(APP_CONFIG_TOKEN);
|
|
564
|
+
|
|
565
|
+
// ❌ Bad
|
|
566
|
+
const port = process.env.PORT; // Not type-safe
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### 4. Request Validation
|
|
570
|
+
|
|
571
|
+
Always validate requests with Zod schemas:
|
|
572
|
+
|
|
573
|
+
```typescript
|
|
574
|
+
// ✅ Good
|
|
575
|
+
@Post()
|
|
576
|
+
async create(@SafeBody(CreateSchema) data: CreateDto) {
|
|
577
|
+
// data is validated and typed
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ❌ Bad
|
|
581
|
+
@Post()
|
|
582
|
+
async create(@Body() data: any) {
|
|
583
|
+
// No validation, no type safety
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### 5. Error Handling
|
|
588
|
+
|
|
589
|
+
Use Result types in domain, exceptions only at edge:
|
|
590
|
+
|
|
591
|
+
```typescript
|
|
592
|
+
// Domain: Result types
|
|
593
|
+
async getUser(id: string): Promise<Result<User, UserNotFoundFailure>> {
|
|
594
|
+
// ...
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Controller: Map to HTTP
|
|
598
|
+
async getUser(@Param('id') id: string) {
|
|
599
|
+
const [user, failure] = await this.service.getUser(id);
|
|
600
|
+
|
|
601
|
+
if (isFailure(UserNotFoundFailure)(failure)) {
|
|
602
|
+
throw new NotFoundException();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return user;
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
## 🔗 Additional Resources
|
|
610
|
+
|
|
611
|
+
### NestJS
|
|
612
|
+
|
|
613
|
+
- [NestJS Documentation](https://docs.nestjs.com/)
|
|
614
|
+
- [NestJS Testing](https://docs.nestjs.com/fundamentals/testing)
|
|
615
|
+
|
|
616
|
+
### Architecture
|
|
617
|
+
|
|
618
|
+
- [Hexagonal Architecture with NestJS](https://blog.octo.com/en/hexagonal-architecture-with-nestjs/)
|
|
619
|
+
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
|
620
|
+
|
|
621
|
+
### Validation
|
|
622
|
+
|
|
623
|
+
- [Zod Documentation](https://zod.dev/)
|
|
624
|
+
- [NestJS Validation](https://docs.nestjs.com/techniques/validation)
|
|
625
|
+
|
|
626
|
+
## 📄 License
|
|
627
|
+
|
|
628
|
+
ISC
|