Getting Started
Project Structure
How to organize a MoroJS project as it grows — from a single file to an enterprise platform. The framework API is the same at every size; you just split things into more files.
The one rule
Everything is built from three primitives. You don’t change frameworks as you scale — you reach for the next one when a file gets crowded:
routes/Auto-loaded files served at their literal path. Great for one-off endpoints like /health or /auth.
modules/Self-contained features registered with loadModule(). Versioned and auto-prefixed at /api/v{version}/{name}.
websockets/Auto-loaded real-time namespaces declared with app.websocket().
Three layouts, one framework
Single file
Everything lives in src/server.ts. Perfect for a prototype, a demo, or a service with a handful of endpoints. Define routes inline with the chainable API.
Directory layout
bash
src/server.ts
typescript
Modular (recommended)
The default for real applications. Group each feature into a module, keep cross-cutting endpoints in routes/, and let the bootstrap file wire it all together. This mirrors the official sample app.
Directory layout
bash
src/modules/users/index.ts — the module
typescript
src/server.ts — the bootstrap
typescript
Every module is just three files. index.ts declares the routes, actions.ts holds the handler functions, and schemas.ts holds the Zod schemas. Add files only when a module outgrows them — which is exactly what the enterprise layout does next.
Enterprise
Same primitives, more structure. As modules grow, split their routes, types, config and middleware into dedicated files, and add top-level folders for the database, shared libraries and operational concerns. Nothing here is a new framework concept — it’s the modular layout with more drawers.
Directory layout
bash
src/modules/auth/index.ts — a richer module
typescript
Notice the module API is identical to the modular layout — defineModule() with a routes array. The only change is that routes.ts, middleware.ts, config.ts and types.ts are now their own files instead of living inside index.ts.
When do I use routes/ vs modules/?
routes/
- Loaded with
app.loadRoutes() - Served at the literal path — no version prefix
- Use
getApp()+app.group()or chaining - Best for
/health,/auth, webhooks, stats
modules/
- Registered with
app.loadModule() - Auto-prefixed at
/api/v{version}/{name} - Declared with
defineModule()— versioned & isolated - Best for versioned business features (users, billing)
Full breakdown: Routes vs Modules.
Configuration files
The same three config files sit at the root of every layout.
package.json
json
moro.config.ts
typescript
tsconfig.json — MoroJS uses native ESM (note the .js imports)
json
Because the project is ESM ("type": "module" with NodeNext), local imports use the compiled .js extension — e.g. import * as actions from './actions.js' — even though the file is .ts.
Best practices
Start small, split later
- Begin with a single file; promote to a module when it grows
- Keep a module to index + actions + schemas until it hurts
- Only add routes.ts / types.ts / config.ts when files get crowded
- Keep Zod schemas next to the actions that use them
Keep boundaries clean
- Business logic lives in actions, not in route definitions
- Versioned features go in modules; shared endpoints in routes/
- Put shared services (db, logger) in services/ and import them
- One domain per module — resist the “misc” module