How to structure a large Express.js application?
Structuring a Large Express.js Application
As Express apps grow, a flat structure becomes unmaintainable. A well-structured app separates concerns, making it testable, scalable, and easy to navigate.
Recommended Folder Structure
src/
βββ app.js β Express app setup (no listen())
βββ server.js β Entry point (starts server)
βββ config/
β βββ index.js β App config (PORT, DB_URL, etc.)
β βββ database.js β DB connection
βββ routes/
β βββ index.js β Root router (mounts all routers)
β βββ users.routes.js
β βββ products.routes.js
βββ controllers/
β βββ users.controller.js
β βββ products.controller.js
βββ services/
β βββ users.service.js β Business logic
β βββ email.service.js
βββ models/
β βββ User.model.js
β βββ Product.model.js
βββ middleware/
β βββ auth.middleware.js
β βββ validate.middleware.js
β βββ rateLimiter.middleware.js
βββ validators/
β βββ users.validator.js
β βββ products.validator.js
βββ utils/
βββ AppError.js
βββ asyncHandler.js
βββ logger.jsapp.js β Pure Express Setup
js
// src/app.js
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const { corsOptions } = require('./config');
const routes = require('./routes');
const { notFound, errorHandler } = require('./middleware/error.middleware');
const app = express();
// Security & parsing
app.use(helmet());
app.use(cors(corsOptions));
app.use(express.json({ limit: '10kb' }));
// Routes
app.use('/api/v1', routes);
// Error handling (always last)
app.use(notFound);
app.use(errorHandler);
module.exports = app;server.js β Entry Point
js
// src/server.js
const app = require('./app');
const { connectDB } = require('./config/database');
const PORT = process.env.PORT || 3000;
async function start() {
await connectDB();
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
}
start().catch(console.error);Routes Layer
js
// src/routes/index.js
const router = require('express').Router();
const usersRoutes = require('./users.routes');
const productsRoutes = require('./products.routes');
router.use('/users', usersRoutes);
router.use('/products', productsRoutes);
module.exports = router;
// src/routes/users.routes.js
const router = require('express').Router();
const { getUsers, getUser, createUser } = require('../controllers/users.controller');
const { authenticate } = require('../middleware/auth.middleware');
const { validateCreateUser } = require('../validators/users.validator');
router.get('/', authenticate, getUsers);
router.get('/:id', authenticate, getUser);
router.post('/', validateCreateUser, createUser);
module.exports = router;Controller Layer
js
// src/controllers/users.controller.js
const asyncHandler = require('../utils/asyncHandler');
const usersService = require('../services/users.service');
const AppError = require('../utils/AppError');
exports.getUsers = asyncHandler(async (req, res) => {
const users = await usersService.findAll(req.query);
res.json({ success: true, data: users });
});
exports.getUser = asyncHandler(async (req, res) => {
const user = await usersService.findById(req.params.id);
if (!user) throw new AppError('User not found', 404);
res.json({ success: true, data: user });
});Service Layer (Business Logic)
js
// src/services/users.service.js
const User = require('../models/User.model');
exports.findAll = async ({ page = 1, limit = 10 }) => {
const skip = (page - 1) * limit;
return User.find().skip(skip).limit(Number(limit));
};
exports.findById = async (id) => {
return User.findById(id);
};
exports.create = async (data) => {
return User.create(data);
};Layers Summary
| Layer | Responsibility |
|---|---|
| Routes | Define URL β handler mapping, apply middleware |
| Controllers | Handle req/res, call services, send response |
| Services | Business logic, database access |
| Models | Data schema and database interface |
| Middleware | Cross-cutting: auth, logging, validation |
| Config | Environment variables, constants |
Summary
Separate your app into routes β controllers β services β models. Keep the Express app in app.js (no listen()) and start it in server.js. This makes the app testable (import app.js in tests without starting the server) and each layer independently maintainable.
Short Answer
Interview readyPremium
A concise answer to help you respond confidently on this topic during an interview.