How to structure a production MERN app that stays maintainable as your team and codebase grow — from folder conventions to API design and error handling.
Most MERN tutorials start you with a flat file structure that works for demos but collapses by the time you add authentication, multiple models, and a team. This is the architecture I use for production MERN applications.
Organise by feature, not by layer. Instead of top-level /controllers, /models, /routes, group everything for a domain together. When you work on the "users" feature, all its files are in one place.
src/
features/
users/
user.model.ts
user.routes.ts
user.controller.ts
user.service.ts
user.validation.ts
tasks/
task.model.ts
task.routes.ts
...
middleware/
auth.ts
errorHandler.ts
config/
db.ts
env.ts
app.ts
server.tsControllers should do one thing: parse the request and call a service. Business logic belongs in the service layer. This makes testing trivial — you test services in isolation without standing up an HTTP server.
Every async route wrapping errors in try/catch is noise. Use an async wrapper utility and a single error handler middleware instead.
// utils/asyncHandler.ts
export const asyncHandler =
(fn: RequestHandler): RequestHandler =>
(req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next)
// middleware/errorHandler.ts
export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
const status = err.statusCode ?? 500
res.status(status).json({ success: false, message: err.message })
}Validate incoming data at the route level before it reaches your service or database. I use Zod on the backend alongside the frontend — same schema, both sides. Define it once, import in both layers.
Never scatter process.env.X across your codebase. Create a single config/env.ts that reads, validates with Zod, and exports all environment variables. Import from there — never from process.env directly.
If your app starts with a missing or malformed environment variable, it should throw immediately with a clear message — not silently fail at runtime when that value is first read.
These patterns add a small amount of upfront structure but pay for themselves by the second sprint. Feature-based folders, service layer, central error handling, and boundary validation are the four changes with the highest return on a growing MERN codebase.