Skip to main content
Practice Problems

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.


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.js

app.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

LayerResponsibility
RoutesDefine URL β†’ handler mapping, apply middleware
ControllersHandle req/res, call services, send response
ServicesBusiness logic, database access
ModelsData schema and database interface
MiddlewareCross-cutting: auth, logging, validation
ConfigEnvironment 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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?
Practice Problems