Routes vs Modules
MoroJS offers two approaches for organizing your API: lightweight routes for app-level concerns and full modules for domain features. Here's when to use each.
On this page
Routes
App-level concerns like auth, health checks, and stats. Lightweight, no versioning.
- Uses
getApp()+app.group() - Auto-loaded via
app.loadRoutes() - Minimal boilerplate
- No lifecycle or versioning overhead
Modules
Domain features like users, blog, orders. Full defineModule() with versioning and middleware.
- API versioning built-in
- Module-level middleware
- Lifecycle hooks (onLoad, onUnload)
- Validation and type safety
Routes
Use routes/ for app-level concerns: health checks, authentication endpoints, stats, and other infrastructure routes. They're lightweight and use getApp() + app.group().
routes/health.ts
typescript
routes/auth/index.ts
typescript
Modules
Use modules/ for domain features: users, blog, orders. Full defineModule() with API versioning, module-level middleware, and validation.
modules/users/index.ts
typescript
When to Choose Modules
/api/v1.0.0/users)Securing Modules by Boundary
Modules support module-level middleware that applies to all routes automatically. Split domains by security boundary — shared handlers and schemas stay together, while access control is enforced at the module level.
modules/blog/index.ts — Split by Security Boundary
typescript
Why Split by Boundary?
- • Security by default — middleware applies to every route in the module, so you can't accidentally expose an admin endpoint
- • Shared code — both modules import from the same
actions.tsandschemas.ts - • Clear intent — reading the module definition immediately tells you the security posture
- • Independent versioning — public and admin APIs can evolve at different rates