How should an Express/TypeScript backend be structured so that HTTP concerns, validation, business logic and data access stay separable and testable, and so multiple services in a monorepo stay consistent as they grow?
Fix: Enforce one strict layering rule per module and never let a layer skip a neighbour. For a module `x`: - `x.routes.ts` — paths and HTTP verbs only, wiring a router to controller methods. No logic. - `x.schema.ts` — Zod schemas plus a parse helper. All external input is validated here, never inside the controller body. - `x.controller.ts` — HTTP in/out only: parse the request via the schema, call the service, map the result to a status code. No business rules, no SQL. - `x.service.ts` — business logic, orchestration, authorization decisions. Knows nothing about `req`/`res`. - `x.repository.ts` — data access only. The only layer that touches the ORM or SQL. Two rules that matter more than the file split itself: 1. Never accept identity from the request body. Derive the acting user from the verified session/token inside the controller and pass it down. A concrete failure this prevents: an endpoint that took `userId` from the JSON body while an upstream gateway auto-injected the internal service key, letting any caller mint a token for any account. The fix was to read the user from the session and ignore the body field entirely. 2. Validate at the boundary with a schema module, so the controller never contains ad hoc `if (!req.body.x)` checks and the same schema can be reused by tests and OpenAPI generation. For the config layer, give every service a Zod-validated config schema and fail fast at boot on a missing variable. Then cross-check the schemas against what the orchestrator actually passes (for compose: `docker compose config --format json`) — a required key present in the schema but absent from compose is a crash loop that only appears in production.
expresstypescriptarchitecturelayeringzodvalidationauthorizationmonorepo
References
- https://github.com/goldbergyoni/nodebestpractices — Node.js best practices recommends structuring the solution into components and using a 3-tier setup that separates the web/controller layer from the service layer and the data access layer, keeping Express objects out of the business logic.
- https://zod.dev/ — Zod is a TypeScript-first schema validation library whose parse/safeParse produce statically typed, validated output, supporting validation of external input at the application boundary.