Skip to Content
How the backend is builtLayers (bottom to top)

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 user
  • admin.middleware.ts — staff permissions
  • internal-key.middleware.ts — worker/cron secret

Layer 7 — index.ts

Wires everything:

  • CORS
  • Global error handler
  • app.route('/api/v1/...', someRouter)
  • connectDB() then serve()

Side folders

FolderLayer 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.env in a model file
  • Duplicating validation only in Flutter with no server check
  • Returning Mongoose documents directly without DTO mapping (leaks internal fields)
Last updated on