@shadow-library/fastify 1.1.0 → 1.3.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,359 @@ 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
+
387
+ ### Global Route Prefix
388
+
389
+ The `routePrefix` configuration option allows you to add a global prefix to all routes in your application. This is useful for:
390
+
391
+ - **API Namespacing**: Prefix all routes with `/api` to clearly distinguish API endpoints from other routes
392
+ - **Multi-tenant Applications**: Add tenant-specific prefixes
393
+ - **Microservices**: Namespace your service routes (e.g., `/user-service`, `/payment-service`)
394
+ - **API Gateways**: Organize routes by domain or service boundaries
395
+
396
+ #### Basic Usage
397
+
398
+ ```typescript
399
+ @Module({
400
+ imports: [
401
+ FastifyModule.forRoot({
402
+ controllers: [UserController, ProductController],
403
+ routePrefix: 'api',
404
+ port: 3000,
405
+ }),
406
+ ],
407
+ })
408
+ export class AppModule {}
409
+ ```
410
+
411
+ **Example Controllers:**
412
+
413
+ ```typescript
414
+ @HttpController('/users')
415
+ export class UserController {
416
+ @Get()
417
+ async getUsers() {
418
+ return [];
419
+ }
420
+
421
+ @Get('/:id')
422
+ async getUser(@Params() params: { id: string }) {
423
+ return { id: params.id };
424
+ }
425
+ }
426
+
427
+ @HttpController('/products')
428
+ export class ProductController {
429
+ @Get()
430
+ async getProducts() {
431
+ return [];
432
+ }
433
+ }
434
+ ```
435
+
436
+ **Result**:
437
+
438
+ - `GET /api/users`
439
+ - `GET /api/users/:id`
440
+ - `GET /api/products`
441
+
442
+ #### Combining with Versioning
443
+
444
+ You can use `routePrefix` together with `prefixVersioning` for a well-organized API structure:
445
+
446
+ ```typescript
447
+ @Module({
448
+ imports: [
449
+ FastifyModule.forRoot({
450
+ controllers: [UserV1Controller, UserV2Controller],
451
+ routePrefix: 'api',
452
+ prefixVersioning: true,
453
+ port: 3000,
454
+ }),
455
+ ],
456
+ })
457
+ export class AppModule {}
458
+ ```
459
+
460
+ **Result**:
461
+
462
+ - `GET /api/v1/users`
463
+ - `GET /api/v2/users`
464
+
465
+ The order is always: `/{routePrefix}/{version}/{controller-path}/{route-path}`
466
+
467
+ #### Multi-Service Example
468
+
469
+ ```typescript
470
+ // User Service Module
471
+ @Module({
472
+ imports: [
473
+ FastifyModule.forRoot({
474
+ controllers: [UserController, AuthController],
475
+ routePrefix: 'user-service',
476
+ port: 3001,
477
+ }),
478
+ ],
479
+ })
480
+ export class UserServiceModule {}
481
+
482
+ // Payment Service Module
483
+ @Module({
484
+ imports: [
485
+ FastifyModule.forRoot({
486
+ controllers: [PaymentController, InvoiceController],
487
+ routePrefix: 'payment-service',
488
+ port: 3002,
489
+ }),
490
+ ],
491
+ })
492
+ export class PaymentServiceModule {}
493
+ ```
494
+
495
+ **Result**:
496
+
497
+ - User Service: `POST /user-service/auth/login`, `GET /user-service/users`
498
+ - Payment Service: `POST /payment-service/payments`, `GET /payment-service/invoices`
499
+
500
+ #### Dynamic Route Prefix
501
+
502
+ You can also set the prefix dynamically using `forRootAsync`:
503
+
504
+ ```typescript
505
+ @Module({
506
+ imports: [
507
+ FastifyModule.forRootAsync({
508
+ useFactory: (configService: ConfigService) => ({
509
+ controllers: [UserController],
510
+ routePrefix: configService.get('API_PREFIX'), // e.g., 'api/v1'
511
+ port: configService.get('PORT'),
512
+ }),
513
+ inject: [ConfigService],
514
+ }),
515
+ ],
516
+ })
517
+ export class AppModule {}
518
+ ```
519
+
520
+ #### Best Practices
521
+
522
+ 1. **Use Consistent Prefixes**: Keep your route prefix consistent across your application
523
+ 2. **Avoid Deep Nesting**: Keep prefixes shallow for better readability (prefer `/api` over `/api/v1/public`)
524
+ 3. **Combine with Versioning**: Use `routePrefix` for namespace and `prefixVersioning` for versions
525
+ 4. **Document Your Routes**: Clearly document the prefix structure for API consumers
526
+ 5. **Environment-based Prefixes**: Consider different prefixes for different environments if needed
527
+
528
+ #### Without Route Prefix
529
+
530
+ If you don't need a global prefix, simply omit the `routePrefix` option:
531
+
532
+ ```typescript
533
+ FastifyModule.forRoot({
534
+ controllers: [UserController],
535
+ // No routePrefix specified
536
+ port: 3000,
537
+ });
538
+ // Routes will be: GET /users (no prefix)
539
+ ```
540
+
186
541
  ### Error Handling
187
542
 
188
543
  ```typescript
@@ -336,6 +691,12 @@ The module provides two configuration methods:
336
691
  // Security
337
692
  maskSensitiveData: true,
338
693
 
694
+ // Global route prefix
695
+ routePrefix: 'api',
696
+
697
+ // API Versioning
698
+ prefixVersioning: true,
699
+
339
700
  // Request ID generation
340
701
  requestIdLogLabel: 'rid',
341
702
  genReqId: () => uuid(),
@@ -16,7 +16,5 @@ const constants_1 = require("../constants.js");
16
16
  * Declaring the constants
17
17
  */
18
18
  function HttpController(path = '') {
19
- if (path.charAt(0) !== '/')
20
- path = `/${path}`;
21
19
  return target => (0, app_1.Controller)({ [constants_1.HTTP_CONTROLLER_TYPE]: 'router', path })(target);
22
20
  }
@@ -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
  };
@@ -50,6 +50,15 @@ export interface FastifyConfig extends FastifyServerOptions {
50
50
  * @default true
51
51
  */
52
52
  maskSensitiveData?: boolean;
53
+ /**
54
+ * Enables prefix-based versioning for all routes in the Fastify instance.
55
+ * @default false
56
+ */
57
+ prefixVersioning?: boolean;
58
+ /**
59
+ * The global route prefix for all routes in the Fastify instance
60
+ */
61
+ routePrefix?: string;
53
62
  }
54
63
  export interface FastifyModuleOptions extends Partial<FastifyConfig> {
55
64
  /**
@@ -67,6 +67,7 @@ export declare class FastifyRouter extends Router {
67
67
  private readonly sensitiveTransformer;
68
68
  constructor(config: FastifyConfig, instance: FastifyInstance, context: ContextService);
69
69
  getInstance(): FastifyInstance;
70
+ private joinPaths;
70
71
  private registerRawBody;
71
72
  private maskField;
72
73
  private getRequestLogger;
@@ -59,6 +59,14 @@ let FastifyRouter = class FastifyRouter extends app_1.Router {
59
59
  getInstance() {
60
60
  return this.instance;
61
61
  }
62
+ joinPaths(...parts) {
63
+ const path = parts
64
+ .filter(p => typeof p === 'string')
65
+ .map(p => p.replace(/^\/+|\/+$/g, ''))
66
+ .filter(Boolean)
67
+ .join('/');
68
+ return `/${path}`;
69
+ }
62
70
  registerRawBody() {
63
71
  const opts = { parseAs: 'buffer' };
64
72
  const parser = this.instance.getDefaultJsonParser('error', 'error');
@@ -117,10 +125,10 @@ let FastifyRouter = class FastifyRouter extends app_1.Router {
117
125
  switch (controller.metadata[constants_1.HTTP_CONTROLLER_TYPE]) {
118
126
  case 'router': {
119
127
  const { instance, metadata, metatype } = controller;
120
- const basePath = metadata.path ?? '/';
121
128
  for (const route of controller.routes) {
122
- const routePath = route.metadata.path ?? '';
123
- const path = basePath + routePath;
129
+ const version = route.metadata.version ?? 1;
130
+ const versionPrefix = this.config.prefixVersioning ? `/v${version}` : '';
131
+ const path = this.joinPaths(this.config.routePrefix, versionPrefix, metadata.path, route.metadata.path);
124
132
  const parsedController = { ...route, instance, metatype };
125
133
  parsedController.metadata.path = path;
126
134
  parsedControllers.routes.push(parsedController);
@@ -13,7 +13,5 @@ import { HTTP_CONTROLLER_TYPE } from '../constants.js';
13
13
  * Declaring the constants
14
14
  */
15
15
  export function HttpController(path = '') {
16
- if (path.charAt(0) !== '/')
17
- path = `/${path}`;
18
16
  return target => Controller({ [HTTP_CONTROLLER_TYPE]: 'router', path })(target);
19
17
  }
@@ -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
  };
@@ -50,6 +50,15 @@ export interface FastifyConfig extends FastifyServerOptions {
50
50
  * @default true
51
51
  */
52
52
  maskSensitiveData?: boolean;
53
+ /**
54
+ * Enables prefix-based versioning for all routes in the Fastify instance.
55
+ * @default false
56
+ */
57
+ prefixVersioning?: boolean;
58
+ /**
59
+ * The global route prefix for all routes in the Fastify instance
60
+ */
61
+ routePrefix?: string;
53
62
  }
54
63
  export interface FastifyModuleOptions extends Partial<FastifyConfig> {
55
64
  /**
@@ -67,6 +67,7 @@ export declare class FastifyRouter extends Router {
67
67
  private readonly sensitiveTransformer;
68
68
  constructor(config: FastifyConfig, instance: FastifyInstance, context: ContextService);
69
69
  getInstance(): FastifyInstance;
70
+ private joinPaths;
70
71
  private registerRawBody;
71
72
  private maskField;
72
73
  private getRequestLogger;
@@ -53,6 +53,14 @@ let FastifyRouter = class FastifyRouter extends Router {
53
53
  getInstance() {
54
54
  return this.instance;
55
55
  }
56
+ joinPaths(...parts) {
57
+ const path = parts
58
+ .filter(p => typeof p === 'string')
59
+ .map(p => p.replace(/^\/+|\/+$/g, ''))
60
+ .filter(Boolean)
61
+ .join('/');
62
+ return `/${path}`;
63
+ }
56
64
  registerRawBody() {
57
65
  const opts = { parseAs: 'buffer' };
58
66
  const parser = this.instance.getDefaultJsonParser('error', 'error');
@@ -111,10 +119,10 @@ let FastifyRouter = class FastifyRouter extends Router {
111
119
  switch (controller.metadata[HTTP_CONTROLLER_TYPE]) {
112
120
  case 'router': {
113
121
  const { instance, metadata, metatype } = controller;
114
- const basePath = metadata.path ?? '/';
115
122
  for (const route of controller.routes) {
116
- const routePath = route.metadata.path ?? '';
117
- const path = basePath + routePath;
123
+ const version = route.metadata.version ?? 1;
124
+ const versionPrefix = this.config.prefixVersioning ? `/v${version}` : '';
125
+ const path = this.joinPaths(this.config.routePrefix, versionPrefix, metadata.path, route.metadata.path);
118
126
  const parsedController = { ...route, instance, metatype };
119
127
  parsedController.metadata.path = path;
120
128
  parsedControllers.routes.push(parsedController);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shadow-library/fastify",
3
3
  "type": "module",
4
- "version": "1.1.0",
4
+ "version": "1.3.0",
5
5
  "sideEffects": false,
6
6
  "description": "A Fastify wrapper featuring decorator-based routing, middleware and error handling",
7
7
  "repository": {