From 524b7d4a4748a04936e08f3ef15c5653cbaaf66c Mon Sep 17 00:00:00 2001 From: Roland Boon Date: Wed, 21 Jan 2026 13:11:43 +0100 Subject: [PATCH] Extends modules to support multiple route files --- docs/application-structure/modules.md | 19 +++++++++++++++++- src/core/bootstrap.ts | 7 +++++-- src/core/types.ts | 5 +++-- test/core/bootstrap.spec.ts | 28 ++++++++++++++++++++++----- test/core/module.spec.ts | 14 ++++++++++++++ 5 files changed, 63 insertions(+), 10 deletions(-) diff --git a/docs/application-structure/modules.md b/docs/application-structure/modules.md index 4ed20e0..f9f1626 100644 --- a/docs/application-structure/modules.md +++ b/docs/application-structure/modules.md @@ -39,10 +39,27 @@ export const UserModule = defineModule({ | `name` | `string` | Unique identifier for the module | | `basePath` | `string` (optional) | Base path for routes (e.g., `'users'` → `/api/users`) | | `providers` | `Provider[]` | Services and dependencies to register | -| `routes` | `RouteFactory` (optional) | Function that defines HTTP routes | +| `routes` | `RouteFactory | RouteFactory[]` (optional) | Function(s) that define HTTP routes | | `imports` | `ModuleConfig[]` (optional) | Other modules this module depends on | | `exports` | `Provider[]` (optional) | Providers to make available to importing modules | +### Multiple Route Files + +Modules can register multiple route files by passing an array: + +```typescript +// src/users/user.module.ts +import { defineModule } from 'glasswork'; +import { userRoutes } from './user.routes'; +import { userAdminRoutes } from './user-admin.routes'; + +export const UserModule = defineModule({ + name: 'user', + providers: [UserService], + routes: [userRoutes, userAdminRoutes], // Multiple route files +}); +``` + ## Feature Modules Feature modules organize code around specific application features. Each feature module encapsulates related functionality: diff --git a/src/core/bootstrap.ts b/src/core/bootstrap.ts index 7bca920..5f4c8fd 100644 --- a/src/core/bootstrap.ts +++ b/src/core/bootstrap.ts @@ -447,8 +447,11 @@ function mountModuleRoutes(options: { // Create a bound route function for this router const boundRoute = [1]>(config: T) => route(router, config); - // Call route factory with router, services, and bound route function - module.routes(router, container.cradle as Record, boundRoute); + // Normalize routes to array and call each factory + const routeFactories = Array.isArray(module.routes) ? module.routes : [module.routes]; + for (const routeFactory of routeFactories) { + routeFactory(router, container.cradle as Record, boundRoute); + } // Mount at base path app.route(`${apiBasePath}/${module.basePath}`, router); diff --git a/src/core/types.ts b/src/core/types.ts index 521a78b..94163f1 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -155,9 +155,10 @@ export interface ModuleConfig { exports?: (Constructor | string)[]; /** - * Route factory function + * Route factory function(s). + * Can be a single factory or an array of factories that will be merged. */ - routes?: RouteFactory; + routes?: RouteFactory | RouteFactory[]; /** * Background jobs registered by this module (optional). diff --git a/test/core/bootstrap.spec.ts b/test/core/bootstrap.spec.ts index 7205920..fb7b736 100644 --- a/test/core/bootstrap.spec.ts +++ b/test/core/bootstrap.spec.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { bootstrap } from '../../src/core/bootstrap.js'; import { defineModule } from '../../src/core/module.js'; -import type { ModuleConfig } from '../../src/core/types.js'; +import type { ModuleConfig, RouteFactory } from '../../src/core/types.js'; import type { ExceptionTracker } from '../../src/observability/exception-tracking.js'; import type { PinoLogger } from '../../src/observability/pino-logger.js'; @@ -152,7 +152,7 @@ describe('bootstrap', () => { }); it('should mount routes when basePath is provided', async () => { - const mockRouteFactory = vi.fn(); + const mockRouteFactory = vi.fn(); const module = defineModule({ name: 'auth', @@ -167,8 +167,26 @@ describe('bootstrap', () => { expect(app).toBeDefined(); }); + it('should support multiple route factories in a single module', async () => { + const mockRouteFactory1 = vi.fn(); + const mockRouteFactory2 = vi.fn(); + + const module = defineModule({ + name: 'test', + basePath: 'test', + providers: [], + routes: [mockRouteFactory1, mockRouteFactory2], + }); + + const { app } = await bootstrap(module); + + expect(mockRouteFactory1).toHaveBeenCalled(); + expect(mockRouteFactory2).toHaveBeenCalled(); + expect(app).toBeDefined(); + }); + it('should not mount routes when basePath is missing', async () => { - const mockRouteFactory = vi.fn(); + const mockRouteFactory = vi.fn(); const module = defineModule({ name: 'test', @@ -189,7 +207,7 @@ describe('bootstrap', () => { } let capturedServices: Record = {}; - const mockRouteFactory = vi.fn((_, services) => { + const mockRouteFactory = vi.fn((_, services) => { capturedServices = services; }); @@ -211,7 +229,7 @@ describe('bootstrap', () => { name: 'test', basePath: 'test', providers: [], - routes: vi.fn(), + routes: vi.fn(), }); const { app } = await bootstrap(module, { apiBasePath: '/v1' }); diff --git a/test/core/module.spec.ts b/test/core/module.spec.ts index 0d6d23e..5c2fcc5 100644 --- a/test/core/module.spec.ts +++ b/test/core/module.spec.ts @@ -227,4 +227,18 @@ describe('defineModule', () => { expect(module.exports).toContain('myService'); }); + + it('should allow multiple route factories', () => { + const route1 = () => {}; + const route2 = () => {}; + const module = defineModule({ + name: 'test', + routes: [route1, route2], + }); + + expect(Array.isArray(module.routes)).toBe(true); + expect(module.routes).toHaveLength(2); + expect(module.routes).toContain(route1); + expect(module.routes).toContain(route2); + }); });