How to organize your Express.js application for scalability and maintainability
📝 Introduction
Express.js is a minimalist and flexible Node.js framework, but as your project grows, a well-structured codebase becomes crucial for maintainability. In this guide, we'll cover the best practices for structuring an Express.js project for clarity, scalability, and maintainability.
📂 Recommended Folder Structure
A clean structure keeps your project modular and scalable. Here's a commonly used Express.js project structure:
📁 my-express-app
├── 📁 src
│ ├── 📁 config # Configuration files (e.g., database, environment variables)
│ ├── 📁 controllers # Business logic (handles requests/responses)
│ ├── 📁 models # Database models & schemas
│ ├── 📁 routes # API route definitions
│ ├── 📁 middlewares # Custom middleware (authentication, logging, error handling)
│ ├── 📁 services # Business logic or external API interactions
│ ├── 📁 utils # Helper functions and utilities
│ ├── app.js # Express app setup
│ └── server.js # Server initialization
├── .env # Environment variables
├── .gitignore # Files to ignore in version control
├── package.json # Dependencies and scripts
├── README.md # Project documentation
1️⃣ Separate Concerns: Use MVC Pattern
The Model-View-Controller (MVC) pattern helps organize code into logical layers:
- Models → Handle database interactions
- Controllers → Contain business logic (handling requests and responses)
- Routes → Define API endpoints
Example:
// src/routes/userRoutes.js
const express = require('express');
const { getUsers, createUser } = require('../controllers/userController');
const router = express.Router();
router.get('/', getUsers);
router.post('/', createUser);
module.exports = router;
// src/controllers/userController.js
const User = require('../models/User');
exports.getUsers = async (req, res) => {
const users = await User.find();
res.json(users);
};
exports.createUser = async (req, res) => {
const newUser = new User(req.body);
await newUser.save();
res.status(201).json(newUser);
};
2️⃣ Use Environment Variables (.env file)
Never hardcode sensitive information like API keys, database credentials, or JWT secrets. Instead, store them in a .env file and load them using dotenv.
Example .env file:
PORT=5000
MONGO_URI=mongodb://localhost:27017/mydb
JWT_SECRET=mysecretkey
Usage in config.js:
require('dotenv').config();
module.exports = {
port: process.env.PORT || 3000,
mongoURI: process.env.MONGO_URI,
jwtSecret: process.env.JWT_SECRET
};
3️⃣ Use Middleware for Code Reusability
Middleware helps keep the main logic clean and reusable.
Example: Logger Middleware
// src/middlewares/logger.js
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
};
module.exports = logger;
Usage in app.js:
const express = require('express');
const logger = require('./middlewares/logger');
const app = express();
app.use(logger);
4️⃣ Implement Proper Error Handling
Centralized error handling prevents redundant error-handling code.
Example: Custom Error Handler
// src/middlewares/errorHandler.js
const errorHandler = (err, req, res, next) => {
res.status(err.status || 500).json({ message: err.message || "Server Error" });
};
module.exports = errorHandler;
Usage in app.js:
const errorHandler = require('./middlewares/errorHandler');
app.use(errorHandler);
5️⃣ Use Services for Business Logic
Keep business logic separate from controllers by using a services layer.
Example: User Service
// src/services/userService.js
const User = require('../models/User');
exports.getAllUsers = async () => {
return await User.find();
};
Controller Usage:
const userService = require('../services/userService');
exports.getUsers = async (req, res) => {
const users = await userService.getAllUsers();
res.json(users);
};
6️⃣ Database Connection in a Separate File
To keep the app.js clean, manage the database connection separately.
Example: Database Connection File
// src/config/db.js
const mongoose = require('mongoose');
const { mongoURI } = require('./config');
const connectDB = async () => {
try {
await mongoose.connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true });
console.log('MongoDB Connected');
} catch (err) {
console.error(err.message);
process.exit(1);
}
};
module.exports = connectDB;
Usage in server.js:
const connectDB = require('./config/db');
connectDB();
🚀 Conclusion
By structuring your Express.js project properly, you create a scalable, maintainable, and organized codebase that grows with your application. Following these best practices ensures your project is easy to debug, extend, and collaborate on.
💡 What structure do you use for your Express.js projects? Let me know in the comments! 🚀
📢 If you found this guide helpful, share it with fellow developers and follow me for more web development tips! You can follow me on GitHub and connect on Twitter
Top comments (4)
dotenvx can replace dotenv, which is more convenient
You are right. Thanks I will use from now.
Because my work is heavily oriented towards final content publication, I rarely use MVC. 85% of the site's "pages" only need to be generated upon change, and only at the selected intervals I assign. (usually when the rss/atom feed changes).
Much of this is CQRS ("command query response system", i.e. only the published item author(s) and editors see command queries capable of CRUD operations. The response system (also htmx related) is used site navigation between the various categories, topics, and articles without full page reloads. This is somewhere between SSR (server-side rendering) and SSG (server-side generation), but is stateless. The HATEOS model works here.
The crossover to MVC/MVVM occurs primarily in two places: specific content types where more extensive TS/JS modules work better: epub and music generation and publishing. Secondarily the e-learning portal I am working on. is entirely MVVM as every learner's progress is unique.
I may add a third MVVM appropriate set of modules later for news, forum, feedback, and commenting, etc., however, those might even be "reddited" etc. by the way.
Thoughts?
Thoughts
Your content is great! By the way, do you happen to make any videos explaining this verbally, or have any video references? I’m still a beginner, and I’d really appreciate it if you have any :)