@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.
Files changed (2) hide show
  1. package/README.md +628 -0
  2. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsfsi-core/ts-nestjs",
3
- "version": "1.1.5",
3
+ "version": "1.1.8",
4
4
  "license": "ISC",
5
5
  "author": "",
6
6
  "type": "module",