Best Practices for Structuring an Express.js Project
Mohamed Ibrahim

Mohamed Ibrahim @moibra

About: Full stack web developer and Technical writer. I teach JavaScript & React & Python & Java

Joined:
Jun 3, 2021

Best Practices for Structuring an Express.js Project

Publish Date: Feb 27
8 2

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  
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
// 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);
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Usage in config.js:

require('dotenv').config();

module.exports = {
    port: process.env.PORT || 3000,
    mongoURI: process.env.MONGO_URI,
    jwtSecret: process.env.JWT_SECRET
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Usage in app.js:

const express = require('express');
const logger = require('./middlewares/logger');

const app = express();
app.use(logger);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Usage in app.js:

const errorHandler = require('./middlewares/errorHandler');
app.use(errorHandler);
Enter fullscreen mode Exit fullscreen mode

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();
};
Enter fullscreen mode Exit fullscreen mode

Controller Usage:

const userService = require('../services/userService');

exports.getUsers = async (req, res) => {
    const users = await userService.getAllUsers();
    res.json(users);
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Usage in server.js:

const connectDB = require('./config/db');
connectDB();
Enter fullscreen mode Exit fullscreen mode

🚀 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

Comments 2 total

  • Uaena_Alex_John
    Uaena_Alex_JohnMay 24, 2025

    dotenvx can replace dotenv, which is more convenient

Add comment