name: ROUTER_MODULE_GUIDE description: Router Module Configuration Guide
Router Module Configuration Guide
Problem
When using RouterModule.register() for path prefixing, controllers inside nested imported modules are NOT recognized.
Example of the Issue:
// ❌ WRONG - Controller won't be recognized by RouterModule
@Module({
imports: [
LibUploadS3Module.registerAsync({...}), // Has UploadS3Controller
],
controllers: [], // Empty - Router won't find controller
})
export class UploadPortalPresentationModule {}
// Router configuration
{
path: 'upload',
module: UploadPortalPresentationModule, // Expected: /v1/portal/upload
}
// Result: Controller routes not prefixed correctly ❌
Solution
Controllers must be declared directly in the module that's registered with RouterModule.
Fixed Implementation:
1. Remove Controller from Library Module
// libs/src/core/upload/s3/upload-s3.module.ts
@Module({})
export class LibUploadS3Module {
public static registerAsync(configure: UploadS3AsyncConfigure): DynamicModule {
return {
module: LibUploadS3Module,
providers: [...],
controllers: [], // ✅ Empty - don't register controllers here
exports: [...],
};
}
}
2. Register Controller in Presentation Module
// src/presentation/portal/upload/upload.module.ts
import { UploadS3Controller } from '@core/upload/s3';
@Module({
imports: [],
controllers: [UploadS3Controller], // ✅ Register controller here
providers: [],
})
export class UploadPortalPresentationModule {}
3. Router Configuration
// src/presentation/portal/portal.module.ts
const routers: Routes = [
{
path: 'portal',
children: [
{
path: 'upload',
module: UploadPortalPresentationModule,
},
// ... other routes
],
},
];
@Module({
imports: [
LibRouterModule.register(routers),
UploadS3InfrastructureModule, // Provides services/providers
// ... other modules
],
})
export class PortalModule {}
Result
✅ Routes are now correctly prefixed:
POST /v1/portal/upload- Upload files endpoint- All controllers in
UploadPortalPresentationModuleget the correct path prefix
Architecture Explanation
Why This Works
┌─────────────────────────────────────────────────────────────┐
│ RouterModule │
│ - Scans modules in routes array │
│ - Only recognizes controllers in those modules │
│ - Does NOT scan nested imports │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ UploadPortalPresentationModule │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ controllers: [UploadS3Controller] ✅ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Separation of Concerns
-
Library Module (
LibUploadS3Module)- Provides services, providers, adapters
- Exports reusable business logic
- No controllers - presentation layer independent
-
Infrastructure Module (
UploadS3InfrastructureModule)- Configures library with environment-specific settings
- Registers multiple scopes (PUBLIC, PRIVATE)
- No controllers - just configuration
-
Presentation Module (
UploadPortalPresentationModule)- Registers controllers for specific route prefix
- Can have its own DTOs, validation
- Presentation layer responsibility
Best Practices
✅ DO
// Presentation module owns its controllers
@Module({
imports: [SomeLibraryModule],
controllers: [MyController], // ✅
})
export class MyPresentationModule {}
❌ DON'T
// Library module should not have controllers
export class LibraryModule {
static forRoot() {
return {
controllers: [LibraryController], // ❌
};
}
}
Multiple Presentation Modules
You can use the same library in different presentation contexts:
// Portal presentation
@Module({
controllers: [UploadS3Controller],
})
export class UploadPortalPresentationModule {}
// Public API presentation
@Module({
controllers: [UploadS3Controller],
})
export class UploadPublicPresentationModule {}
// Internal API presentation
@Module({
controllers: [UploadS3Controller],
})
export class UploadInternalPresentationModule {}
// Router configuration
const routers: Routes = [
{ path: 'portal/upload', module: UploadPortalPresentationModule },
{ path: 'public/upload', module: UploadPublicPresentationModule },
{ path: 'internal/upload', module: UploadInternalPresentationModule },
];
Result:
POST /v1/portal/upload- Portal uploadPOST /v1/public/upload- Public uploadPOST /v1/internal/upload- Internal upload
Debugging Tips
Check registered routes
# Enable NestJS debug logging
LOG_LEVEL=debug npm run start:dev
Look for logs like:
[RouterExplorer] Mapped {/v1/portal/upload, POST} route
Verify controller registration
import { NestFactory } from '@nestjs/core';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Log all routes
const server = app.getHttpServer();
const router = server._events.request._router;
console.log(router.stack
.filter(r => r.route)
.map(r => ({
path: r.route.path,
methods: Object.keys(r.route.methods),
}))
);
}
Common Mistakes
1. Controller in wrong module
// ❌ Controller in library module
LibUploadS3Module.registerAsync({
controllers: [UploadS3Controller], // Wrong!
});
// ✅ Controller in presentation module
@Module({
controllers: [UploadS3Controller], // Correct!
})
export class UploadPortalPresentationModule {}
2. Forgetting to import library
// ❌ No library imports
@Module({
controllers: [UploadS3Controller], // Controller exists
// But no providers/services available!
})
// ✅ Import library for services
@Module({
imports: [UploadS3InfrastructureModule], // Provides services
controllers: [UploadS3Controller],
})
3. Wrong router configuration
// ❌ Wrong - using infrastructure module
{
path: 'upload',
module: UploadS3InfrastructureModule, // Has no controllers!
}
// ✅ Correct - using presentation module
{
path: 'upload',
module: UploadPortalPresentationModule, // Has controllers!
}
Related Files
/libs/src/core/upload/s3/upload-s3.module.ts- Library module (no controllers)/libs/src/core/upload/s3/upload-s3.controller.ts- Shared controller/src/infrastructure/upload-s3-service/upload-s3.module.ts- Infrastructure setup/src/presentation/portal/upload/upload.module.ts- Presentation module (has controllers)/src/presentation/portal/portal.module.ts- Router configuration