@shadow-library/fastify 1.0.1 → 1.2.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 CHANGED
@@ -14,6 +14,7 @@ A powerful TypeScript-first Fastify wrapper featuring decorator-based routing, a
14
14
  - 📊 **Type Safety**: Full TypeScript support with schema generation
15
15
  - 🎨 **Templating Ready**: Built-in support for templating engines
16
16
  - ⚡ **Dynamic Module**: Configurable module with `forRoot()` and `forRootAsync()` methods
17
+ - 🔢 **API Versioning**: Built-in support for prefix-based API versioning
17
18
 
18
19
  ## Installation
19
20
 
@@ -129,6 +130,7 @@ bootstrap();
129
130
  @Delete(path?: string) // DELETE requests
130
131
  @Options(path?: string) // OPTIONS requests
131
132
  @Head(path?: string) // HEAD requests
133
+ @Version(version: number) // Set API version for the route
132
134
  ```
133
135
 
134
136
  #### Parameter Decorators
@@ -183,6 +185,205 @@ export class ProtectedController {
183
185
  }
184
186
  ```
185
187
 
188
+ ### API Versioning
189
+
190
+ The framework provides a powerful versioning system that allows you to version your APIs using URL path prefixes. This is useful for maintaining multiple versions of your API simultaneously.
191
+
192
+ #### Enabling Versioning
193
+
194
+ To enable versioning, set `prefixVersioning: true` in your module configuration:
195
+
196
+ ```typescript
197
+ @Module({
198
+ imports: [
199
+ FastifyModule.forRoot({
200
+ controllers: [UserController],
201
+ port: 3000,
202
+
203
+ // Enable prefix-based versioning
204
+ prefixVersioning: true,
205
+ }),
206
+ ],
207
+ })
208
+ export class AppModule {}
209
+ ```
210
+
211
+ #### Using the @Version Decorator
212
+
213
+ Use the `@Version` decorator on your controllers to specify the API version:
214
+
215
+ ```typescript
216
+ import { HttpController, Get, Post, Version, Body } from '@shadow-library/fastify';
217
+
218
+ @HttpController('/api/users')
219
+ @Version(1)
220
+ export class UserV1Controller {
221
+ @Get()
222
+ async getUsers() {
223
+ return [{ id: 1, name: 'John Doe' }];
224
+ }
225
+ }
226
+
227
+ @HttpController('/api/users')
228
+ @Version(2)
229
+ export class UserV2Controller {
230
+ @Get()
231
+ async getUsers() {
232
+ // v2 with additional fields
233
+ return [{ id: 1, name: 'John Doe', email: 'john@example.com', createdAt: new Date() }];
234
+ }
235
+
236
+ @Post()
237
+ async createUser(@Body() userData: CreateUserDto) {
238
+ // New features in v2
239
+ return { id: 2, ...userData };
240
+ }
241
+ }
242
+ ```
243
+
244
+ **Result**:
245
+
246
+ - v1 endpoints: `GET /v1/api/users`
247
+ - v2 endpoints: `GET /v2/api/users`, `POST /v2/api/users`
248
+
249
+ #### Default Version
250
+
251
+ If versioning is enabled but no `@Version` decorator is specified, routes default to `v1`:
252
+
253
+ ```typescript
254
+ @HttpController('/api/products')
255
+ export class ProductController {
256
+ @Get()
257
+ async getProducts() {
258
+ return [];
259
+ }
260
+ }
261
+ // Results in: GET /v1/api/products
262
+ ```
263
+
264
+ #### Method-Level Versioning
265
+
266
+ You can also apply versioning at the method level for more granular control:
267
+
268
+ ```typescript
269
+ @HttpController('/api/data')
270
+ export class DataController {
271
+ @Get('/stats')
272
+ @Version(1)
273
+ async getStatsV1() {
274
+ return { totalUsers: 100 };
275
+ }
276
+
277
+ @Get('/stats')
278
+ @Version(2)
279
+ async getStatsV2() {
280
+ return { totalUsers: 100, activeUsers: 75, newUsers: 10 };
281
+ }
282
+
283
+ @Get('/info')
284
+ async getInfo() {
285
+ // This defaults to v1 when versioning is enabled
286
+ return { version: 'v1' };
287
+ }
288
+ }
289
+ ```
290
+
291
+ **Result**:
292
+
293
+ - `GET /v1/api/data/stats` → `getStatsV1()`
294
+ - `GET /v2/api/data/stats` → `getStatsV2()`
295
+ - `GET /v1/api/data/info` → `getInfo()`
296
+
297
+ #### Versioning Best Practices
298
+
299
+ 1. **Maintain Backward Compatibility**: Keep older versions running while you migrate clients to newer versions
300
+ 2. **Version Breaking Changes**: Only increment versions for breaking changes in your API
301
+ 3. **Document Version Differences**: Clearly document what changes between versions
302
+ 4. **Deprecation Strategy**: Communicate deprecation timelines for older API versions
303
+ 5. **Consistent Versioning**: Use consistent version numbers across related endpoints
304
+
305
+ #### Example: Complete Versioned API
306
+
307
+ ```typescript
308
+ import { Module } from '@shadow-library/app';
309
+ import { FastifyModule, HttpController, Get, Post, Version } from '@shadow-library/fastify';
310
+
311
+ // Version 1 Controllers
312
+ @HttpController('/api/users')
313
+ @Version(1)
314
+ class UserV1Controller {
315
+ @Get()
316
+ async list() {
317
+ return [{ id: 1, name: 'John' }];
318
+ }
319
+ }
320
+
321
+ @HttpController('/api/posts')
322
+ @Version(1)
323
+ class PostV1Controller {
324
+ @Get()
325
+ async list() {
326
+ return [{ id: 1, title: 'Hello' }];
327
+ }
328
+ }
329
+
330
+ // Version 2 Controllers - with enhanced features
331
+ @HttpController('/api/users')
332
+ @Version(2)
333
+ class UserV2Controller {
334
+ @Get()
335
+ async list() {
336
+ return [{ id: 1, name: 'John', email: 'john@example.com', role: 'admin' }];
337
+ }
338
+
339
+ @Post('/bulk')
340
+ async bulkCreate(@Body() users: CreateUserDto[]) {
341
+ // New feature in v2
342
+ return { created: users.length };
343
+ }
344
+ }
345
+
346
+ @HttpController('/api/posts')
347
+ @Version(2)
348
+ class PostV2Controller {
349
+ @Get()
350
+ async list() {
351
+ return [{ id: 1, title: 'Hello', content: 'World', tags: ['news'] }];
352
+ }
353
+ }
354
+
355
+ @Module({
356
+ imports: [
357
+ FastifyModule.forRoot({
358
+ controllers: [UserV1Controller, UserV2Controller, PostV1Controller, PostV2Controller],
359
+ prefixVersioning: true,
360
+ port: 3000,
361
+ }),
362
+ ],
363
+ })
364
+ export class AppModule {}
365
+
366
+ // Available endpoints:
367
+ // GET /v1/api/users
368
+ // GET /v1/api/posts
369
+ // GET /v2/api/users
370
+ // POST /v2/api/users/bulk (new in v2)
371
+ // GET /v2/api/posts
372
+ ```
373
+
374
+ #### Disabling Versioning
375
+
376
+ If you don't need versioning, simply omit the `prefixVersioning` option or set it to `false`:
377
+
378
+ ```typescript
379
+ FastifyModule.forRoot({
380
+ controllers: [UserController],
381
+ prefixVersioning: false, // or omit entirely
382
+ port: 3000,
383
+ });
384
+ // Routes will be: GET /api/users (no version prefix)
385
+ ```
386
+
186
387
  ### Error Handling
187
388
 
188
389
  ```typescript
@@ -223,6 +424,89 @@ export class RoutesController {
223
424
  }
224
425
  ```
225
426
 
427
+ #### Child Routes Configuration
428
+
429
+ Child routes enable server-side route resolution, commonly used for SSR (Server-Side Rendering) and internal API composition. When enabled, you can make internal HTTP requests to your own routes without going through the network layer.
430
+
431
+ **Basic Setup:**
432
+
433
+ ```typescript
434
+ @Module({
435
+ imports: [
436
+ FastifyModule.forRoot({
437
+ controllers: [UserController, DataController],
438
+
439
+ // Enable child routes functionality
440
+ enableChildRoutes: true,
441
+
442
+ // Optional: Provide custom headers for child route requests
443
+ childRouteHeaders: () => ({
444
+ 'x-correlation-id': '123',
445
+ }),
446
+ }),
447
+ ],
448
+ })
449
+ export class AppModule {}
450
+ ```
451
+
452
+ **Usage in Controllers:**
453
+
454
+ ```typescript
455
+ @HttpController('/api')
456
+ export class DataAggregatorController {
457
+ constructor(@Inject(Router) private readonly fastifyRouter: FastifyRouter) {}
458
+
459
+ @Get('/dashboard')
460
+ async getDashboardData() {
461
+ // Make internal requests to other routes
462
+ const [users, posts, analytics] = await Promise.all([
463
+ this.fastifyRouter.resolveChildRoute('/api/users'),
464
+ this.fastifyRouter.resolveChildRoute('/api/posts?limit=10'),
465
+ this.fastifyRouter.resolveChildRoute('/api/analytics/summary'),
466
+ ]);
467
+
468
+ return {
469
+ dashboard: {
470
+ users,
471
+ posts,
472
+ analytics,
473
+ timestamp: new Date().toISOString(),
474
+ },
475
+ };
476
+ }
477
+ }
478
+ ```
479
+
480
+ **Custom Headers Function:**
481
+
482
+ The `childRouteHeaders` function is called for each child route request, allowing you to:
483
+
484
+ - Pass authentication context from the parent request
485
+ - Include tenant/user-specific information
486
+ - Add tracing or correlation IDs
487
+ - Set internal service flags
488
+
489
+ ```typescript
490
+ // Dynamic headers based on current request context
491
+ childRouteHeaders: (contextService) => {
492
+ const request = contextService.getRequest();
493
+ return {
494
+ 'x-user-id': contextService.get('currentUserId'),
495
+ 'x-request-id': contextService.getRID(),
496
+ 'x-forwarded-from': 'internal-aggregator',
497
+ 'x-correlation-id': request.headers['x-correlation-id'],
498
+ };
499
+ },
500
+ ```
501
+
502
+ **Important Notes:**
503
+
504
+ - Child routes always include the header `x-service: 'internal-child-route'`
505
+ - Custom headers are merged with the default service header
506
+ - If you provide an `x-service` header, it will be overridden with the default value
507
+ - Child routes create isolated contexts, preventing middleware conflicts
508
+ - Enable only when needed, as it adds routing overhead
509
+
226
510
  ## Configuration
227
511
 
228
512
  ### Dynamic Module Configuration
@@ -253,6 +537,9 @@ The module provides two configuration methods:
253
537
  // Security
254
538
  maskSensitiveData: true,
255
539
 
540
+ // API Versioning
541
+ prefixVersioning: true,
542
+
256
543
  // Request ID generation
257
544
  requestIdLogLabel: 'rid',
258
545
  genReqId: () => uuid(),
@@ -269,6 +556,12 @@ The module provides two configuration methods:
269
556
  '5xx': ErrorResponseSchema,
270
557
  },
271
558
 
559
+ // Child routes configuration (for SSR and internal route resolution)
560
+ enableChildRoutes: true,
561
+ childRouteHeaders: contextService => ({
562
+ 'x-correlation-id': contextService.getRequest().headers['x-correlation-id'],
563
+ }),
564
+
272
565
  // Extend Fastify instance before registering controllers
273
566
  fastifyFactory: async fastify => {
274
567
  // Register plugins, add hooks, or configure Fastify
@@ -800,7 +1093,7 @@ Check out the [examples](./examples) directory for complete working examples:
800
1093
 
801
1094
  - **hello-world**: Basic HTTP controller with GET/POST routes
802
1095
  - **user-auth**: Advanced example with authentication guards
803
- - **child-routes**: Route resolution and unified endpoints
1096
+ - **child-routes**: Route resolution, unified endpoints, and custom headers for SSR
804
1097
 
805
1098
  ## Contributing
806
1099
 
@@ -4,3 +4,4 @@ export * from './http-output.decorator.js';
4
4
  export * from './http-route.decorator.js';
5
5
  export * from './middleware.decorator.js';
6
6
  export * from './sensitive.decorator.js';
7
+ export * from './version.decorator.js';
@@ -20,3 +20,4 @@ __exportStar(require("./http-output.decorator.js"), exports);
20
20
  __exportStar(require("./http-route.decorator.js"), exports);
21
21
  __exportStar(require("./middleware.decorator.js"), exports);
22
22
  __exportStar(require("./sensitive.decorator.js"), exports);
23
+ __exportStar(require("./version.decorator.js"), exports);
@@ -0,0 +1,11 @@
1
+ import { Integer } from 'type-fest';
2
+ /**
3
+ * Importing user defined packages
4
+ */
5
+ /**
6
+ * Defining types
7
+ */
8
+ /**
9
+ * Declaring the constants
10
+ */
11
+ export declare function Version<T extends number>(version: Integer<T>): ClassDecorator & MethodDecorator;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Version = Version;
4
+ /**
5
+ * Importing npm packages
6
+ */
7
+ const app_1 = require("@shadow-library/app");
8
+ /**
9
+ * Importing user defined packages
10
+ */
11
+ /**
12
+ * Defining types
13
+ */
14
+ /**
15
+ * Declaring the constants
16
+ */
17
+ function Version(version) {
18
+ return (0, app_1.Route)({ version });
19
+ }
@@ -16,6 +16,7 @@ declare module '@shadow-library/app' {
16
16
  interface RouteMetadata extends Omit<RouteShorthandOptions, 'config'> {
17
17
  method?: HttpMethod;
18
18
  path?: string;
19
+ version?: number;
19
20
  schemas?: RouteInputSchemas & {
20
21
  response?: Record<number | string, JSONSchema>;
21
22
  };
@@ -9,6 +9,7 @@ import { Promisable } from 'type-fest';
9
9
  * Importing user defined packages
10
10
  */
11
11
  import { ErrorHandler } from '../interfaces/index.js';
12
+ import { ContextService } from '../services/index.js';
12
13
  /**
13
14
  * Defining types
14
15
  */
@@ -39,11 +40,21 @@ export interface FastifyConfig extends FastifyServerOptions {
39
40
  * @default false
40
41
  */
41
42
  enableChildRoutes?: boolean;
43
+ /**
44
+ * Function to provide custom headers for internal child route requests.
45
+ * Useful for passing authentication tokens or other necessary headers.
46
+ */
47
+ childRouteHeaders?: (contextService: ContextService) => Record<string, string>;
42
48
  /**
43
49
  * Masks fields marked as sensitive in API inputs (body, query, and URL params) when written to logs.
44
50
  * @default true
45
51
  */
46
52
  maskSensitiveData?: boolean;
53
+ /**
54
+ * Enables prefix-based versioning for all routes in the Fastify instance.
55
+ * @default false
56
+ */
57
+ prefixVersioning?: boolean;
47
58
  }
48
59
  export interface FastifyModuleOptions extends Partial<FastifyConfig> {
49
60
  /**
@@ -82,7 +82,7 @@ export declare class FastifyRouter extends Router {
82
82
  * during SSR. Automatically reuses middleware results from the parent context to avoid
83
83
  * redundant execution and ensures correct context isolation for nested route data fetching.
84
84
  */
85
- resolveChildRoute<T extends JsonValue = JsonObject>(url: string): Promise<T>;
85
+ resolveChildRoute<T extends JsonValue = JsonObject>(url: string, headers?: Record<string, string>): Promise<T>;
86
86
  mockRequest(): MockRequestChain;
87
87
  mockRequest(options: MockRequestOptions): Promise<MockResponse>;
88
88
  }
@@ -119,8 +119,14 @@ let FastifyRouter = class FastifyRouter extends app_1.Router {
119
119
  const { instance, metadata, metatype } = controller;
120
120
  const basePath = metadata.path ?? '/';
121
121
  for (const route of controller.routes) {
122
+ /** Prepare path with versioning if enabled */
122
123
  const routePath = route.metadata.path ?? '';
123
- const path = basePath + routePath;
124
+ let path = basePath + routePath;
125
+ if (this.config.prefixVersioning) {
126
+ const version = route.metadata.version ?? 1;
127
+ const connector = path.startsWith('/') ? '' : '/';
128
+ path = '/v' + version + connector + path;
129
+ }
124
130
  const parsedController = { ...route, instance, metatype };
125
131
  parsedController.metadata.path = path;
126
132
  parsedControllers.routes.push(parsedController);
@@ -285,10 +291,12 @@ let FastifyRouter = class FastifyRouter extends app_1.Router {
285
291
  * during SSR. Automatically reuses middleware results from the parent context to avoid
286
292
  * redundant execution and ensures correct context isolation for nested route data fetching.
287
293
  */
288
- async resolveChildRoute(url) {
294
+ async resolveChildRoute(url, headers = {}) {
289
295
  if (!this.childRouter)
290
296
  throw new common_1.InternalError('Child routes are not enabled');
291
- const response = await this.instance.inject({ method: 'GET', url, headers: { 'x-service': 'internal-child-route' } });
297
+ const childHeaders = this.config.childRouteHeaders?.(this.context) ?? {};
298
+ Object.assign(headers, childHeaders, { 'x-service': 'internal-child-route' });
299
+ const response = await this.instance.inject({ method: 'GET', url, headers });
292
300
  return response.json();
293
301
  }
294
302
  mockRequest(options) {
@@ -4,3 +4,4 @@ export * from './http-output.decorator.js';
4
4
  export * from './http-route.decorator.js';
5
5
  export * from './middleware.decorator.js';
6
6
  export * from './sensitive.decorator.js';
7
+ export * from './version.decorator.js';
@@ -4,3 +4,4 @@ export * from './http-output.decorator.js';
4
4
  export * from './http-route.decorator.js';
5
5
  export * from './middleware.decorator.js';
6
6
  export * from './sensitive.decorator.js';
7
+ export * from './version.decorator.js';
@@ -0,0 +1,11 @@
1
+ import { Integer } from 'type-fest';
2
+ /**
3
+ * Importing user defined packages
4
+ */
5
+ /**
6
+ * Defining types
7
+ */
8
+ /**
9
+ * Declaring the constants
10
+ */
11
+ export declare function Version<T extends number>(version: Integer<T>): ClassDecorator & MethodDecorator;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Importing npm packages
3
+ */
4
+ import { Route } from '@shadow-library/app';
5
+ /**
6
+ * Importing user defined packages
7
+ */
8
+ /**
9
+ * Defining types
10
+ */
11
+ /**
12
+ * Declaring the constants
13
+ */
14
+ export function Version(version) {
15
+ return Route({ version });
16
+ }
@@ -16,6 +16,7 @@ declare module '@shadow-library/app' {
16
16
  interface RouteMetadata extends Omit<RouteShorthandOptions, 'config'> {
17
17
  method?: HttpMethod;
18
18
  path?: string;
19
+ version?: number;
19
20
  schemas?: RouteInputSchemas & {
20
21
  response?: Record<number | string, JSONSchema>;
21
22
  };
@@ -9,6 +9,7 @@ import { Promisable } from 'type-fest';
9
9
  * Importing user defined packages
10
10
  */
11
11
  import { ErrorHandler } from '../interfaces/index.js';
12
+ import { ContextService } from '../services/index.js';
12
13
  /**
13
14
  * Defining types
14
15
  */
@@ -39,11 +40,21 @@ export interface FastifyConfig extends FastifyServerOptions {
39
40
  * @default false
40
41
  */
41
42
  enableChildRoutes?: boolean;
43
+ /**
44
+ * Function to provide custom headers for internal child route requests.
45
+ * Useful for passing authentication tokens or other necessary headers.
46
+ */
47
+ childRouteHeaders?: (contextService: ContextService) => Record<string, string>;
42
48
  /**
43
49
  * Masks fields marked as sensitive in API inputs (body, query, and URL params) when written to logs.
44
50
  * @default true
45
51
  */
46
52
  maskSensitiveData?: boolean;
53
+ /**
54
+ * Enables prefix-based versioning for all routes in the Fastify instance.
55
+ * @default false
56
+ */
57
+ prefixVersioning?: boolean;
47
58
  }
48
59
  export interface FastifyModuleOptions extends Partial<FastifyConfig> {
49
60
  /**
@@ -82,7 +82,7 @@ export declare class FastifyRouter extends Router {
82
82
  * during SSR. Automatically reuses middleware results from the parent context to avoid
83
83
  * redundant execution and ensures correct context isolation for nested route data fetching.
84
84
  */
85
- resolveChildRoute<T extends JsonValue = JsonObject>(url: string): Promise<T>;
85
+ resolveChildRoute<T extends JsonValue = JsonObject>(url: string, headers?: Record<string, string>): Promise<T>;
86
86
  mockRequest(): MockRequestChain;
87
87
  mockRequest(options: MockRequestOptions): Promise<MockResponse>;
88
88
  }
@@ -113,8 +113,14 @@ let FastifyRouter = class FastifyRouter extends Router {
113
113
  const { instance, metadata, metatype } = controller;
114
114
  const basePath = metadata.path ?? '/';
115
115
  for (const route of controller.routes) {
116
+ /** Prepare path with versioning if enabled */
116
117
  const routePath = route.metadata.path ?? '';
117
- const path = basePath + routePath;
118
+ let path = basePath + routePath;
119
+ if (this.config.prefixVersioning) {
120
+ const version = route.metadata.version ?? 1;
121
+ const connector = path.startsWith('/') ? '' : '/';
122
+ path = '/v' + version + connector + path;
123
+ }
118
124
  const parsedController = { ...route, instance, metatype };
119
125
  parsedController.metadata.path = path;
120
126
  parsedControllers.routes.push(parsedController);
@@ -279,10 +285,12 @@ let FastifyRouter = class FastifyRouter extends Router {
279
285
  * during SSR. Automatically reuses middleware results from the parent context to avoid
280
286
  * redundant execution and ensures correct context isolation for nested route data fetching.
281
287
  */
282
- async resolveChildRoute(url) {
288
+ async resolveChildRoute(url, headers = {}) {
283
289
  if (!this.childRouter)
284
290
  throw new InternalError('Child routes are not enabled');
285
- const response = await this.instance.inject({ method: 'GET', url, headers: { 'x-service': 'internal-child-route' } });
291
+ const childHeaders = this.config.childRouteHeaders?.(this.context) ?? {};
292
+ Object.assign(headers, childHeaders, { 'x-service': 'internal-child-route' });
293
+ const response = await this.instance.inject({ method: 'GET', url, headers });
286
294
  return response.json();
287
295
  }
288
296
  mockRequest(options) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shadow-library/fastify",
3
3
  "type": "module",
4
- "version": "1.0.1",
4
+ "version": "1.2.0",
5
5
  "sideEffects": false,
6
6
  "description": "A Fastify wrapper featuring decorator-based routing, middleware and error handling",
7
7
  "repository": {