Express Project Structure
When you start building applications with Express.js, one of the most critical decisions you'll make is how to structure your project. A well-organized project structure makes your code easier to maintain, extend, and collaborate on with others. This guide will help you understand common Express project structures and best practices.
Why Project Structure Matters
Before diving into specific structures, let's understand why project structure is important:
- Maintainability: Makes it easier to find and update code
- Scalability: Allows your application to grow without becoming chaotic
- Collaboration: Helps team members understand where things belong
- Onboarding: Reduces the learning curve for new developers
- Testing: Facilitates isolated testing of components
Basic Express Project Structure
Let's start with a basic Express project structure that's suitable for small applications:
my-express-app/
├── node_modules/
├── public/
│ ├── css/
│ ├── js/
│ └── images/
├── views/
├── routes/
├── app.js
├── package.json
└── package-lock.json
Key Components
Let's break down these components:
app.js
(or index.js
)
This is the entry point of your application where you set up your Express server:
const express = require('express');
const path = require('path');
const app = express();
const port = process.env.PORT || 3000;
// Middleware setup
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
// Route setup
app.use('/users', require('./routes/users'));
app.use('/products', require('./routes/products'));
// Default route
app.get('/', (req, res) => {
res.send('Hello World!');
});
// Start the server
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
routes/
Directory
This directory contains route handlers for different paths in your application:
// routes/users.js
const express = require('express');
const router = express.Router();
// GET all users
router.get('/', (req, res) => {
res.send('Get all users');
});
// GET a specific user
router.get('/:id', (req, res) => {
res.send(`Get user with ID ${req.params.id}`);
});
module.exports = router;
public/
Directory
This folder stores static assets like CSS, JavaScript, and images that are directly accessible by clients.
views/
Directory
If your application serves HTML views (using template engines like EJS, Pug, or Handlebars), they're stored here.
Intermediate Project Structure
As your application grows, you'll want a more organized structure to separate concerns and improve maintainability:
my-express-app/
├── config/
│ └── db.js
├── controllers/
│ ├── userController.js
│ └── productController.js
├── middleware/
│ ├── auth.js
│ └── errorHandler.js
├── models/
│ ├── User.js
│ └── Product.js
├── public/
│ ├── css/
│ ├── js/
│ └── images/
├── routes/
│ ├── api/
│ │ ├── users.js
│ │ └── products.js
│ └── index.js
├── services/
│ └── emailService.js
├── utils/
│ └── helpers.js
├── views/
├── app.js
├── package.json
└── package-lock.json
Key Components
Let's explore the additional components in this structure:
config/
Directory
Contains configuration files, such as database connections or environment variables:
// config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI);
console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDB;
models/
Directory
Contains data models for your application:
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', userSchema);
controllers/
Directory
Contains logic for handling routes, separating route definitions from their implementation:
// controllers/userController.js
const User = require('../models/User');
// Get all users
exports.getUsers = async (req, res) => {
try {
const users = await User.find().select('-password');
res.json(users);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
};
// Get user by ID
exports.getUserById = async (req, res) => {
try {
const user = await User.findById(req.params.id).select('-password');
if (!user) {
return res.status(404).json({ msg: 'User not found' });
}
res.json(user);
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
};
middleware/
Directory
Contains middleware functions for authentication, error handling, etc.:
// middleware/auth.js
const jwt = require('jsonwebtoken');
module.exports = function(req, res, next) {
// Get token from header
const token = req.header('x-auth-token');
// Check if no token
if (!token) {
return res.status(401).json({ msg: 'No token, authorization denied' });
}
// Verify token
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded.user;
next();
} catch (err) {
res.status(401).json({ msg: 'Token is not valid' });
}
};
Advanced MVC Project Structure
For larger applications, the Model-View-Controller (MVC) pattern provides a clean separation of concerns:
my-express-app/
├── app/
│ ├── controllers/
│ │ ├── api/
│ │ └── web/
│ ├── middleware/
│ ├── models/
│ ├── services/
│ └── views/
├── config/
│ ├── database.js
│ ├── express.js
│ └── routes.js
├── public/
├── tests/
│ ├── integration/
│ └── unit/
├── .env
├── .gitignore
├── app.js
├── package.json
└── README.md
Real-world Example: Building a RESTful API
Let's see how a project structure works in practice by building a simple RESTful API for a blog:
blog-api/
├── config/
│ ├── db.js
│ └── default.json
├── controllers/
│ ├── authController.js
│ ├── postController.js
│ └── userController.js
├── middleware/
│ ├── auth.js
│ └── errorHandler.js
├── models/
│ ├── Post.js
│ └── User.js
├── routes/
│ ├── api/
│ │ ├── auth.js
│ │ ├── posts.js
│ │ └── users.js
│ └── index.js
├── app.js
├── package.json
└── README.md
Implementation
Setting Up the Main App File
// app.js
const express = require('express');
const connectDB = require('./config/db');
// Initialize Express
const app = express();
// Connect to Database
connectDB();
// Init Middleware
app.use(express.json());
// Define Routes
app.use('/api/users', require('./routes/api/users'));
app.use('/api/auth', require('./routes/api/auth'));
app.use('/api/posts', require('./routes/api/posts'));
// Error handling middleware
app.use(require('./middleware/errorHandler'));
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
Creating a Route File
// routes/api/posts.js
const express = require('express');
const router = express.Router();
const auth = require('../../middleware/auth');
const postController = require('../../controllers/postController');
// @route GET api/posts
// @desc Get all posts
// @access Public
router.get('/', postController.getAllPosts);
// @route GET api/posts/:id
// @desc Get post by ID
// @access Public
router.get('/:id', postController.getPostById);
// @route POST api/posts
// @desc Create a post
// @access Private
router.post('/', auth, postController.createPost);
// @route DELETE api/posts/:id
// @desc Delete a post
// @access Private
router.delete('/:id', auth, postController.deletePost);
module.exports = router;
Creating a Controller
// controllers/postController.js
const Post = require('../models/Post');
// Get all posts
exports.getAllPosts = async (req, res, next) => {
try {
const posts = await Post.find().sort({ date: -1 });
res.json(posts);
} catch (err) {
next(err);
}
};
// Get post by ID
exports.getPostById = async (req, res, next) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ msg: 'Post not found' });
}
res.json(post);
} catch (err) {
next(err);
}
};
// Create a post
exports.createPost = async (req, res, next) => {
try {
const newPost = new Post({
title: req.body.title,
content: req.body.content,
user: req.user.id
});
const post = await newPost.save();
res.json(post);
} catch (err) {
next(err);
}
};
// Delete a post
exports.deletePost = async (req, res, next) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({ msg: 'Post not found' });
}
// Check if user owns the post
if (post.user.toString() !== req.user.id) {
return res.status(401).json({ msg: 'User not authorized' });
}
await post.remove();
res.json({ msg: 'Post removed' });
} catch (err) {
next(err);
}
};
Best Practices for Express Project Structure
-
Separation of Concerns: Keep different aspects of your application in separate files and folders.
-
Route Organization: Group related routes together and separate API routes from web routes.
-
Middleware Management: Keep middleware organized and documented, especially custom middleware.
-
Configuration Separation: Store configuration separately from code to make it environment-agnostic.
-
Model-View-Controller (MVC): Use the MVC pattern to separate data, user interface, and control logic.
-
Modularization: Break down large files into smaller, more focused modules.
-
Naming Conventions: Be consistent with file and folder naming conventions.
-
Error Handling: Implement centralized error handling to avoid code duplication.
Common Mistakes to Avoid
-
Overly Flat Structure: Putting everything in the root directory makes it hard to find files.
-
Overly Nested Structure: Too many nested folders can be confusing and make paths too long.
-
Inconsistent Naming: Mixing naming conventions like camelCase, snake_case, and kebab-case.
-
Business Logic in Routes: Routes should be thin and delegate to controllers.
-
Circular Dependencies: Be careful not to create circular dependencies between modules.
Summary
A well-structured Express project is crucial for maintaining and scaling your application. Start with a simple structure for small projects and evolve it as your application grows. The MVC pattern provides a solid foundation for organizing code, but adapt it to your specific needs.
Remember that there's no one-size-fits-all approach to project structure. The best structure for your application depends on its size, complexity, and your team's preferences.
Additional Resources
Exercises
-
Create a simple Express application with a basic project structure that serves static files.
-
Refactor an existing Express application to follow the MVC pattern.
-
Create a RESTful API for a simple resource (e.g., products, tasks) using the project structure described in this guide.
-
Add middleware for authentication and error handling to your Express application.
-
Set up a test environment for your Express application, with separate configuration for development and production environments.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)