@shadow-library/fastify 1.2.0 → 1.4.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
@@ -384,6 +384,160 @@ FastifyModule.forRoot({
384
384
  // Routes will be: GET /api/users (no version prefix)
385
385
  ```
386
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
+
387
541
  ### Error Handling
388
542
 
389
543
  ```typescript
@@ -537,6 +691,9 @@ The module provides two configuration methods:
537
691
  // Security
538
692
  maskSensitiveData: true,
539
693
 
694
+ // Global route prefix
695
+ routePrefix: 'api',
696
+
540
697
  // API Versioning
541
698
  prefixVersioning: true,
542
699
 
@@ -693,6 +850,55 @@ export class AppModule {}
693
850
  - **Database**: Add database connection decorators
694
851
  - **Caching**: Configure caching plugins
695
852
 
853
+ ### Customizing AJV Validation
854
+
855
+ The module uses [AJV](https://ajv.js.org/) for JSON Schema validation. You can customize the AJV instance by providing custom options or plugins through the `ajv` configuration:
856
+
857
+ ```typescript
858
+ import { Module } from '@shadow-library/app';
859
+ import { FastifyModule } from '@shadow-library/fastify';
860
+ import ajvFormats from 'ajv-formats';
861
+ import ajvKeywords from 'ajv-keywords';
862
+
863
+ @Module({
864
+ imports: [
865
+ FastifyModule.forRoot({
866
+ controllers: [UserController],
867
+ ajv: {
868
+ // Custom AJV options
869
+ customOptions: {
870
+ removeAdditional: 'all',
871
+ coerceTypes: 'array',
872
+ useDefaults: true,
873
+ },
874
+ // AJV plugins - supports both formats:
875
+ // 1. Just the plugin function (uses empty options)
876
+ // 2. Tuple of [plugin, options]
877
+ plugins: [
878
+ ajvFormats, // Plugin without options
879
+ [ajvKeywords, { keywords: ['typeof', 'instanceof'] }], // Plugin with options
880
+ ],
881
+ },
882
+ }),
883
+ ],
884
+ })
885
+ export class AppModule {}
886
+ ```
887
+
888
+ #### AJV Configuration Options
889
+
890
+ | Option | Type | Description |
891
+ | --------------- | ------------------------------------------ | ----------------------------------------------------- |
892
+ | `customOptions` | `object` | Custom AJV options to merge with the default settings |
893
+ | `plugins` | `Array<Plugin \| [Plugin, PluginOptions]>` | Array of AJV plugins to register with both validators |
894
+
895
+ #### Common Use Cases for AJV Customization:
896
+
897
+ - **Format Validation**: Add `ajv-formats` for email, uri, date-time validation
898
+ - **Custom Keywords**: Use `ajv-keywords` for additional validation keywords
899
+ - **Strict Mode**: Configure strict schema validation settings
900
+ - **Type Coercion**: Customize how types are coerced during validation
901
+
696
902
  ## Middleware
697
903
 
698
904
  Create custom middleware by implementing the `Middleware` decorator:
@@ -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
  }
@@ -55,6 +55,10 @@ export interface FastifyConfig extends FastifyServerOptions {
55
55
  * @default false
56
56
  */
57
57
  prefixVersioning?: boolean;
58
+ /**
59
+ * The global route prefix for all routes in the Fastify instance
60
+ */
61
+ routePrefix?: string;
58
62
  }
59
63
  export interface FastifyModuleOptions extends Partial<FastifyConfig> {
60
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,16 +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
- /** Prepare path with versioning if enabled */
123
- const routePath = route.metadata.path ?? '';
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
- }
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);
130
132
  const parsedController = { ...route, instance, metatype };
131
133
  parsedController.metadata.path = path;
132
134
  parsedControllers.routes.push(parsedController);
@@ -1,9 +1,16 @@
1
1
  import { ValidationError } from '@shadow-library/common';
2
- import { SchemaObject } from 'ajv';
2
+ import Ajv, { SchemaObject } from 'ajv';
3
3
  import { FastifyInstance } from 'fastify';
4
4
  import { FastifyRouteSchemaDef, FastifySchemaValidationError, FastifyValidationResult, SchemaErrorDataVar } from 'fastify/types/schema';
5
5
  import { FastifyConfig, FastifyModuleOptions } from './fastify-module.interface.js';
6
+ /**
7
+ * Defining types
8
+ */
9
+ export interface AjvValidators {
10
+ strictValidator: Ajv;
11
+ lenientValidator: Ajv;
12
+ }
6
13
  export declare const notFoundHandler: () => never;
7
- export declare function compileValidator(routeSchema: FastifyRouteSchemaDef<SchemaObject>): FastifyValidationResult;
14
+ export declare function compileValidator(routeSchema: FastifyRouteSchemaDef<SchemaObject>, validators: AjvValidators): FastifyValidationResult;
8
15
  export declare function formatSchemaErrors(errors: FastifySchemaValidationError[], dataVar: SchemaErrorDataVar): ValidationError;
9
16
  export declare function createFastifyInstance(config: FastifyConfig, fastifyFactory?: FastifyModuleOptions['fastifyFactory']): Promise<FastifyInstance>;
@@ -18,17 +18,13 @@ const fastify_1 = require("fastify");
18
18
  * Importing user defined packages
19
19
  */
20
20
  const server_error_1 = require("../server.error.js");
21
- /**
22
- * Defining types
23
- */
24
21
  /**
25
22
  * Declaring the constants
26
23
  */
27
24
  const keywords = ['x-fastify'];
28
25
  const allowedHttpParts = ['body', 'params', 'querystring'];
29
- const strictValidator = new ajv_1.default({ allErrors: true, useDefaults: true, removeAdditional: true, strict: true, keywords });
30
- const lenientValidator = new ajv_1.default({ allErrors: true, coerceTypes: true, useDefaults: true, removeAdditional: true, strict: true, keywords });
31
26
  const notFoundError = new server_error_1.ServerError(server_error_1.ServerErrorCode.S002);
27
+ const defaultAjvOptions = { allErrors: true, useDefaults: true, removeAdditional: true, strict: true, keywords };
32
28
  const notFoundHandler = () => (0, common_1.throwError)(notFoundError);
33
29
  exports.notFoundHandler = notFoundHandler;
34
30
  function compileSchema(ajv, schema) {
@@ -41,13 +37,13 @@ function compileSchema(ajv, schema) {
41
37
  }
42
38
  return ajv.getSchema(schema.$id);
43
39
  }
44
- function compileValidator(routeSchema) {
40
+ function compileValidator(routeSchema, validators) {
45
41
  (0, node_assert_1.default)(allowedHttpParts.includes(routeSchema.httpPart), `Invalid httpPart: ${routeSchema.httpPart}`);
46
42
  if (routeSchema.httpPart === 'body')
47
- return compileSchema(strictValidator, routeSchema.schema);
43
+ return compileSchema(validators.strictValidator, routeSchema.schema);
48
44
  if (routeSchema.httpPart === 'params')
49
- return compileSchema(lenientValidator, routeSchema.schema);
50
- const validate = compileSchema(lenientValidator, routeSchema.schema);
45
+ return compileSchema(validators.lenientValidator, routeSchema.schema);
46
+ const validate = compileSchema(validators.lenientValidator, routeSchema.schema);
51
47
  return (data) => {
52
48
  validate(data);
53
49
  for (const error of validate.errors ?? []) {
@@ -78,10 +74,19 @@ function formatSchemaErrors(errors, dataVar) {
78
74
  async function createFastifyInstance(config, fastifyFactory) {
79
75
  const options = common_1.utils.object.omitKeys(config, ['port', 'host', 'errorHandler', 'responseSchema']);
80
76
  const { errorHandler } = config;
77
+ const strictValidator = new ajv_1.default({ ...defaultAjvOptions, ...config.ajv?.customOptions });
78
+ const lenientValidator = new ajv_1.default({ ...defaultAjvOptions, coerceTypes: true, ...config.ajv?.customOptions });
79
+ for (let plugin of config.ajv?.plugins ?? []) {
80
+ if (typeof plugin === 'function')
81
+ plugin = [plugin, {}];
82
+ const [ajvPlugin, options] = plugin;
83
+ ajvPlugin(strictValidator, options);
84
+ ajvPlugin(lenientValidator, options);
85
+ }
81
86
  const instance = (0, fastify_1.fastify)(options);
82
87
  instance.setSchemaErrorFormatter(formatSchemaErrors);
83
88
  instance.setNotFoundHandler(exports.notFoundHandler);
84
89
  instance.setErrorHandler(errorHandler.handle.bind(errorHandler));
85
- instance.setValidatorCompiler(compileValidator);
90
+ instance.setValidatorCompiler(routeSchema => compileValidator(routeSchema, { strictValidator, lenientValidator }));
86
91
  return fastifyFactory ? await fastifyFactory(instance) : instance;
87
92
  }
@@ -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
  }
@@ -55,6 +55,10 @@ export interface FastifyConfig extends FastifyServerOptions {
55
55
  * @default false
56
56
  */
57
57
  prefixVersioning?: boolean;
58
+ /**
59
+ * The global route prefix for all routes in the Fastify instance
60
+ */
61
+ routePrefix?: string;
58
62
  }
59
63
  export interface FastifyModuleOptions extends Partial<FastifyConfig> {
60
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,16 +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
- /** Prepare path with versioning if enabled */
117
- const routePath = route.metadata.path ?? '';
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
- }
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);
124
126
  const parsedController = { ...route, instance, metatype };
125
127
  parsedController.metadata.path = path;
126
128
  parsedControllers.routes.push(parsedController);
@@ -1,9 +1,16 @@
1
1
  import { ValidationError } from '@shadow-library/common';
2
- import { SchemaObject } from 'ajv';
2
+ import Ajv, { SchemaObject } from 'ajv';
3
3
  import { FastifyInstance } from 'fastify';
4
4
  import { FastifyRouteSchemaDef, FastifySchemaValidationError, FastifyValidationResult, SchemaErrorDataVar } from 'fastify/types/schema';
5
5
  import { FastifyConfig, FastifyModuleOptions } from './fastify-module.interface.js';
6
+ /**
7
+ * Defining types
8
+ */
9
+ export interface AjvValidators {
10
+ strictValidator: Ajv;
11
+ lenientValidator: Ajv;
12
+ }
6
13
  export declare const notFoundHandler: () => never;
7
- export declare function compileValidator(routeSchema: FastifyRouteSchemaDef<SchemaObject>): FastifyValidationResult;
14
+ export declare function compileValidator(routeSchema: FastifyRouteSchemaDef<SchemaObject>, validators: AjvValidators): FastifyValidationResult;
8
15
  export declare function formatSchemaErrors(errors: FastifySchemaValidationError[], dataVar: SchemaErrorDataVar): ValidationError;
9
16
  export declare function createFastifyInstance(config: FastifyConfig, fastifyFactory?: FastifyModuleOptions['fastifyFactory']): Promise<FastifyInstance>;
@@ -9,17 +9,13 @@ import { fastify } from 'fastify';
9
9
  * Importing user defined packages
10
10
  */
11
11
  import { ServerError, ServerErrorCode } from '../server.error.js';
12
- /**
13
- * Defining types
14
- */
15
12
  /**
16
13
  * Declaring the constants
17
14
  */
18
15
  const keywords = ['x-fastify'];
19
16
  const allowedHttpParts = ['body', 'params', 'querystring'];
20
- const strictValidator = new Ajv({ allErrors: true, useDefaults: true, removeAdditional: true, strict: true, keywords });
21
- const lenientValidator = new Ajv({ allErrors: true, coerceTypes: true, useDefaults: true, removeAdditional: true, strict: true, keywords });
22
17
  const notFoundError = new ServerError(ServerErrorCode.S002);
18
+ const defaultAjvOptions = { allErrors: true, useDefaults: true, removeAdditional: true, strict: true, keywords };
23
19
  export const notFoundHandler = () => throwError(notFoundError);
24
20
  function compileSchema(ajv, schema) {
25
21
  if (!schema.$id)
@@ -31,13 +27,13 @@ function compileSchema(ajv, schema) {
31
27
  }
32
28
  return ajv.getSchema(schema.$id);
33
29
  }
34
- export function compileValidator(routeSchema) {
30
+ export function compileValidator(routeSchema, validators) {
35
31
  assert(allowedHttpParts.includes(routeSchema.httpPart), `Invalid httpPart: ${routeSchema.httpPart}`);
36
32
  if (routeSchema.httpPart === 'body')
37
- return compileSchema(strictValidator, routeSchema.schema);
33
+ return compileSchema(validators.strictValidator, routeSchema.schema);
38
34
  if (routeSchema.httpPart === 'params')
39
- return compileSchema(lenientValidator, routeSchema.schema);
40
- const validate = compileSchema(lenientValidator, routeSchema.schema);
35
+ return compileSchema(validators.lenientValidator, routeSchema.schema);
36
+ const validate = compileSchema(validators.lenientValidator, routeSchema.schema);
41
37
  return (data) => {
42
38
  validate(data);
43
39
  for (const error of validate.errors ?? []) {
@@ -68,10 +64,19 @@ export function formatSchemaErrors(errors, dataVar) {
68
64
  export async function createFastifyInstance(config, fastifyFactory) {
69
65
  const options = utils.object.omitKeys(config, ['port', 'host', 'errorHandler', 'responseSchema']);
70
66
  const { errorHandler } = config;
67
+ const strictValidator = new Ajv({ ...defaultAjvOptions, ...config.ajv?.customOptions });
68
+ const lenientValidator = new Ajv({ ...defaultAjvOptions, coerceTypes: true, ...config.ajv?.customOptions });
69
+ for (let plugin of config.ajv?.plugins ?? []) {
70
+ if (typeof plugin === 'function')
71
+ plugin = [plugin, {}];
72
+ const [ajvPlugin, options] = plugin;
73
+ ajvPlugin(strictValidator, options);
74
+ ajvPlugin(lenientValidator, options);
75
+ }
71
76
  const instance = fastify(options);
72
77
  instance.setSchemaErrorFormatter(formatSchemaErrors);
73
78
  instance.setNotFoundHandler(notFoundHandler);
74
79
  instance.setErrorHandler(errorHandler.handle.bind(errorHandler));
75
- instance.setValidatorCompiler(compileValidator);
80
+ instance.setValidatorCompiler(routeSchema => compileValidator(routeSchema, { strictValidator, lenientValidator }));
76
81
  return fastifyFactory ? await fastifyFactory(instance) : instance;
77
82
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shadow-library/fastify",
3
3
  "type": "module",
4
- "version": "1.2.0",
4
+ "version": "1.4.0",
5
5
  "sideEffects": false,
6
6
  "description": "A Fastify wrapper featuring decorator-based routing, middleware and error handling",
7
7
  "repository": {