Project Structure
How you organize your files shapes how quickly new developers find things, how well concerns stay separated, and how painlessly the codebase scales. There's no single "correct" Node project layout, but there are two dominant strategies — feature-based (group by domain/feature) and layer-based (group by technical role) — and a handful of conventions that nearly every serious project shares: separating source from output, keeping entry points thin, co-locating tests, and giving each piece of the stack a predictable home. This page covers both strategies, the file and folder conventions that apply to either, and how to decide which to use.
The two main strategies
LAYER-BASED (technical role) FEATURE-BASED (domain / business concept)
src/ src/
routes/ users/
users.ts users.router.ts
orders.ts users.service.ts
controllers/ users.repository.ts
users.controller.ts users.types.ts
orders.controller.ts users.test.ts
services/ orders/
users.service.ts orders.router.ts
orders.service.ts orders.service.ts
repositories/ ...
users.repository.ts shared/
models/ db.ts
... logger.tsA typical Express project layout
src/ app.ts ← create the Express app (no .listen) — testable without a server server.ts ← import app, call .listen() — the real entry point config.ts ← validated, frozen config object routes/ ← (or feature folders) router files controllers/ ← thin: parse req, call service, send res services/ ← business logic, no HTTP knowledge repositories/ ← all database access, no business logic models/ ← TypeScript types / Zod schemas / ORM models middleware/ ← auth, logging, error handler, rate limiter utils/ ← pure helper functions dist/ ← compiled output (git-ignored) tests/ ← (or __tests__/ next to each module) .env.example ← documents required variables (no real values) package.json tsconfig.json
File and folder conventions
Co-locate tests next to the code they test (
users.service.test.tsbesideusers.service.ts) — easier to find, less likely to go stale than a distanttests/tree.Keep entry points thin (
server.tsshould be ~10 lines: import app, call listen, handle startup errors).One file, one concern — a 600-line file is a smell; split it along logical boundaries before it gets there.
index.tsbarrels in each module folder export the public API, letting consumers import fromusers/instead ofusers/users.service.Name files consistently —
*.router.ts,*.service.ts,*.repository.ts,*.test.ts— so the type is obvious from the name.shared/orcommon/for utilities, config, and middleware used across features — never import from a sibling feature directory.