Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/application-structure/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 5 additions & 2 deletions src/core/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,11 @@ function mountModuleRoutes(options: {
// Create a bound route function for this router
const boundRoute = <T extends Parameters<typeof route>[1]>(config: T) => route(router, config);

// Call route factory with router, services, and bound route function
module.routes(router, container.cradle as Record<string, unknown>, 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<string, unknown>, boundRoute);
}

// Mount at base path
app.route(`${apiBasePath}/${module.basePath}`, router);
Expand Down
5 changes: 3 additions & 2 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
28 changes: 23 additions & 5 deletions test/core/bootstrap.spec.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -152,7 +152,7 @@ describe('bootstrap', () => {
});

it('should mount routes when basePath is provided', async () => {
const mockRouteFactory = vi.fn();
const mockRouteFactory = vi.fn<RouteFactory>();

const module = defineModule({
name: 'auth',
Expand All @@ -167,8 +167,26 @@ describe('bootstrap', () => {
expect(app).toBeDefined();
});

it('should support multiple route factories in a single module', async () => {
const mockRouteFactory1 = vi.fn<RouteFactory>();
const mockRouteFactory2 = vi.fn<RouteFactory>();

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<RouteFactory>();

const module = defineModule({
name: 'test',
Expand All @@ -189,7 +207,7 @@ describe('bootstrap', () => {
}

let capturedServices: Record<string, unknown> = {};
const mockRouteFactory = vi.fn((_, services) => {
const mockRouteFactory = vi.fn<RouteFactory>((_, services) => {
capturedServices = services;
});

Expand All @@ -211,7 +229,7 @@ describe('bootstrap', () => {
name: 'test',
basePath: 'test',
providers: [],
routes: vi.fn(),
routes: vi.fn<RouteFactory>(),
});

const { app } = await bootstrap(module, { apiBasePath: '/v1' });
Expand Down
14 changes: 14 additions & 0 deletions test/core/module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});