How to Build a Production-Ready REST API with Express: Structure and Error Handling
A production-ready Express API is more than routes that return data — it needs clear structure, input validation, consistent responses, and centralised error handling. Get these foundations right and your API stays maintainable and reliable as it grows.
Structure it in layers
Avoid piling everything into one file. A clean separation:
- Routes — define endpoints and delegate to controllers.
- Controllers — handle the request/response, call services.
- Services — hold business logic, independent of Express.
- Data layer — database access, using connection pooling.
This makes code testable and easy to navigate.
Use middleware well
Middleware handles cross-cutting concerns in order: body parsing, security headers, authentication, request logging, rate limiting. Keep the order deliberate, since each runs in sequence.
Validate input
Validate request bodies and parameters against a schema at the edge, rejecting bad input early with a clear 400 response. This keeps invalid data out of your logic and blocks injection and pollution.
Centralise error handling
Express recognises error-handling middleware by its four arguments. Put one at the end to catch everything:
app.use((err, req, res, next) => { logger.error(err); res.status(err.status || 500).json({ error: err.message }); });
Forward errors to it with next(err), return generic messages to clients, and keep details in your logs. Handle async errors too — see handling rejections.
Consistent responses and status codes
Return meaningful HTTP status codes (200, 201, 400, 401, 404, 500) and a consistent JSON shape, so clients can rely on a predictable contract.
Frequently asked questions
How does Express know a middleware handles errors?
By its signature — error-handling middleware takes four arguments (err, req, res, next). Define it last, and pass errors to it with next(err) from your routes.
How do I catch errors in async routes?
Wrap async handlers so rejected Promises are forwarded to your error middleware (a small wrapper or an async-error helper does this). Otherwise an async error can slip past Express's default handling.
What status code should I return on failure?
Use the code that fits: 400 for bad input, 401/403 for auth, 404 for missing resources, 500 for unexpected server errors. Consistent, accurate codes make your API predictable to consume.
Was this article helpful?