DEV Community

Mohamed Ibrahim
Mohamed Ibrahim

Posted on

Best Practices for Structuring an Express.js Project

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

Top comments (0)