Express is minimal by design — which means the architecture is entirely up to you. Here's the structure I use to keep Express APIs maintainable as they grow past a handful of routes.
Node.js and Express remain one of the most productive stacks for building APIs — but that minimalism is a double-edged sword. Without deliberate structure, an Express app turns into a pile of routes with logic scattered across files. Here's the architecture that scales.
src/
routes/ -> route definitions, thin, no business logic
controllers/ -> request/response handling
services/ -> business logic, framework-agnostic
repositories/ -> database access
middleware/ -> auth, validation, error handling
config/ -> env, database, third-party clients
utils/
The key rule: controllers stay thin, services hold logic, repositories own data access. This keeps business logic testable without spinning up an HTTP server.
app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({
error: { message: err.expose ? err.message : "Internal server error", code: err.code }
});
});
Never leak stack traces or raw database errors to clients. Throw typed errors (ValidationError,
NotFoundError) from services and translate them centrally.
If your controllers have try/catch blocks scattered everywhere, your error handling architecture is missing, not your try/catch discipline.
Validate every request body, query param, and route param before it reaches business logic — schema validation libraries (Zod, Joi) turn this into a declarative, type-safe step rather than manual if-checks.
* with credentials)Express doesn't enforce architecture — that's the job of the engineer building on it. A clean layered structure, centralized error handling, and boundary validation are what separate a prototype from a production API.
I'm Sandaruwan Jayasundara — Senior Software Engineer | Backend Specialist. Explore my projects or get in touch about your backend.