Express Server Folder Structure Best Practices
A clean Express server structure saves rework. Here is the layout that works.
Express Server Folder Structure Best Practices
A good folder structure makes an Express server easier to grow. Bad structure creates spaghetti code by month two.
The Layout
src/
config/
index.js
middlewares/
auth.js
error.js
validate.js
models/
user.js
routes/
auth.js
user.js
controllers/
authController.js
userController.js
utils/
db.js
logger.js
app.js
server.js
.env
What Each File Does
src/app.js
Creates the Express app, applies middlewares (json, cors, helmet), mounts routers, and registers the error handler. Does not call listen. This keeps app and server separate.
src/server.js
Imports app, connects to the database, and calls app.listen. This file is about running the server, not about Express setup.
src/config/index.js
Loads env vars with dotenv and exports them as a typed config object. All env access goes here. No other file should read process.env directly.
src/routes/
Declares endpoints and binds them to controllers. Keep routes thin: one line per route.
router.post('/signup', signup);
router.post('/login', login);
src/controllers/
Handles request/response logic. Parses the request, calls a service or model, sends the response. Keep controllers thin. Move complex logic to a services folder if needed.
src/models/
Mongoose schemas and models. Each file defines one collection: user.js defines the User model and exports it.
src/middlewares/
Reusable middlewares: auth (verify JWT), validate (validate body with zod), error (catch errors). One file per middleware.
src/utils/
Shared helpers: db connection, logger, email sender. Anything multiple modules use.
Why Split app.js and server.js
You separate the Express app (app.js) from the running server (server.js) for two reasons. Testing: you can import app without listening on a port. Flexibility: you can swap how the server starts (HTTP, HTTPS, with a cluster) without touching the app setup.
The Takeaway
A clean Express structure separates config, routes, controllers, models, middlewares, and utils. Keep app.js and server.js separate for testing and flexibility. One file, one job. This keeps the server maintainable as features grow.
Separate folders for config, middlewares, models, routes, controllers, and utils. Keep app.js and server.js separate. One file per concern. This is the cleanest layout for a growing Express backend.
So you can import the Express app in tests without listening on a port, and so you can swap the server startup (HTTP, HTTPS, cluster) without touching the app. app.js creates the app; server.js starts it.
Env var loading with dotenv, exported as a typed config object. By convention, all env access goes through this file so other files do not read process.env directly. This keeps config centralized.
Thin. Parse the request, call a service or model, send the response. If a controller has complex logic, move that logic to a services folder. Controllers orchestrate; they do not contain business logic.
In a utils folder. Things like the database connection, logger, email sender, and other helpers used by multiple modules live in utils.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

