Layers (bottom to top)
Think of the backend as a stack of plates. Each plate only talks to the plate below.
Layer 1 — MongoDB
Raw storage. Collections like users, practicepyqquestions, etc.
You never write SQL here — it’s document JSON.
Layer 2 — models/
What shape is each document?
Mongoose schemas: fields, types, defaults, indexes.
Models should be dumb: describe data, not business workflows.
Layer 3 — services/
What are we allowed to do?
Examples:
- Schedule next SRS review date
- Send FCM push if user opted in
- Verify Google ID token and upsert user
Services call models and other services. They do not know about HTTP headers.
Layer 4 — validators/
Is the JSON body well-formed?
Zod schemas: required fields, enums, max lengths.
Validators do not check “does this user own this row?” — that’s service/auth work.
Layer 5 — routes/
HTTP adapter.
Parse request → validate → call service → return c.json(...).
Routes should stay thin. If a route grows huge, move logic to a service.
Layer 6 — middleware/
Cross-cutting gates on many routes:
auth.middleware.ts— logged-in useradmin.middleware.ts— staff permissionsinternal-key.middleware.ts— worker/cron secret
Layer 7 — index.ts
Wires everything:
- CORS
- Global error handler
app.route('/api/v1/...', someRouter)connectDB()thenserve()
Side folders
| Folder | Layer role |
|---|---|
config/ | Boot-time settings + DB connect |
utils/ | Pure helpers (dates, DTO mapping, URL parsing) |
lib/ | Tiny shared non-domain helpers |
constants/ | Fixed enums shared across modules |
types/ | TypeScript-only contracts (no runtime) |
Anti-patterns (please avoid)
- Putting SRS math in a route handler
- Reading
process.envin a model file - Duplicating validation only in Flutter with no server check
- Returning Mongoose documents directly without DTO mapping (leaks internal fields)