Express File Organization
When building Express.js applications, having a clear and maintainable file structure is crucial for project scalability. This guide will help you understand best practices for organizing your Express application files and directories.
Introduction
File organization might seem like a minor concern when you start a new project, but as your application grows, a well-structured codebase becomes essential. Good organization helps with:
- Maintainability: Makes code easier to update and debug
- Collaboration: Helps team members quickly understand where to find specific functionality
- Scalability: Allows the application to grow without becoming unwieldy
Let's explore different approaches to organizing Express applications, from simple to more complex architectures.
Basic Express File Structure
For small applications or learning projects, a simple structure works well:
project-root/
├── node_modules/
├── public/ # Static files (CSS, images, client-side JS)
├── views/ # Template files
├── routes/ # Route handlers
├── app.js # Main application file
└── package.json
Here's how to set up this basic structure:
- First, create your project and install Express:
mkdir my-express-app
cd my-express-app
npm init -y
npm install express
- Create your main
app.js
file:
const express = require('express');
const path = require('path');
const app = express();
const port = 3000;
// Middleware for parsing request bodies
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Serve static files
app.use(express.static(path.join(__dirname, 'public')));
// Set up template engine
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs'); // or other template engine
// Routes
app.get('/', (req, res) => {
res.render('index', { title: 'Home' });
});
// Start server
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Organizing Routes
As your application grows, you'll want to separate routes into their own files. Create a routes
directory:
routes/
├── index.js
├── users.js
└── products.js
In each route file, export a router:
// routes/users.js
const express = require('express');
const router = express.Router();
// Get all users
router.get('/', (req, res) => {
res.send('Get all users');
});
// Get specific user
router.get('/:id', (req, res) => {
res.send(`Get user with ID: ${req.params.id}`);
});
module.exports = router;
Then in your main app.js
, import and use these routes:
// Import routes
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
const productsRouter = require('./routes/products');
// Use routes
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/products', productsRouter);
MVC Pattern in Express
For larger applications, the Model-View-Controller (MVC) pattern provides better organization:
project-root/
├── controllers/ # Logic for handling requests
├── models/ # Data models and database logic
├── views/ # Template files
├── routes/ # Route definitions
├── public/ # Static files
├── middlewares/ # Custom middleware functions
├── config/ # Configuration files
├── utils/ # Helper functions
├── app.js # Main application file
└── package.json
Let's implement a simple user management system using this structure:
- Create a model:
// models/userModel.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', userSchema);
- Create a controller:
// controllers/userController.js
const User = require('../models/userModel');
exports.getAllUsers = async (req, res) => {
try {
const users = await User.find();
res.status(200).json({
status: 'success',
results: users.length,
data: { users }
});
} catch (err) {
res.status(500).json({
status: 'error',
message: err.message
});
}
};
exports.createUser = async (req, res) => {
try {
const newUser = await User.create(req.body);
res.status(201).json({
status: 'success',
data: { user: newUser }
});
} catch (err) {
res.status(400).json({
status: 'fail',
message: err.message
});
}
};
- Set up routes:
// routes/userRoutes.js
const express = require('express');
const userController = require('../controllers/userController');
const router = express.Router();
router
.route('/')
.get(userController.getAllUsers)
.post(userController.createUser);
module.exports = router;
- Connect everything in
app.js
:
const express = require('express');
const mongoose = require('mongoose');
const userRoutes = require('./routes/userRoutes');
const app = express();
// Middleware
app.use(express.json());
// Routes
app.use('/api/users', userRoutes);
// Connect to database
mongoose.connect('mongodb://localhost:27017/express-demo', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('DB connection successful!'));
// Start server
const port = 3000;
app.listen(port, () => {
console.log(`App running on port ${port}...`);
});
Feature-Based Organization
For complex applications, you might prefer organizing by feature rather than technical role:
project-root/
├── features/
│ ├── users/
│ │ ├── user.model.js
│ │ ├── user.controller.js
│ │ ├── user.routes.js
│ │ └── user.test.js
│ └── products/
│ ├── product.model.js
│ ├── product.controller.js
│ ├── product.routes.js
│ └── product.test.js
├── common/
│ ├── middlewares/
│ └── utils/
├── config/
├── app.js
└── package.json
This approach groups all files related to a feature together, making it easier to:
- Find all code related to a specific feature
- Understand how the feature works as a whole
- Maintain or update the feature without jumping between directories
Best Practices for File Organization
Regardless of which structure you choose, follow these best practices:
- Be consistent: Once you choose a pattern, stick with it throughout the application
- Keep files focused: Each file should have a single responsibility
- Use descriptive names: Name files and folders based on what they do, not how they do it
- Separate configuration: Keep configuration parameters in dedicated files
- Group middleware: Store custom middleware functions in their own directory
- Create utility functions: Extract common functionality into helper/utility files
- Document your structure: Include a README.md explaining your file organization
Environment Configuration
Create a dedicated config
directory to store environment variables and configuration:
config/
├── index.js
├── database.js
└── .env
In config/index.js
:
require('dotenv').config();
module.exports = {
port: process.env.PORT || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
database: {
url: process.env.DATABASE_URL,
options: {
useNewUrlParser: true,
useUnifiedTopology: true
}
},
jwtSecret: process.env.JWT_SECRET
};
Then import this configuration wherever needed:
const config = require('./config');
app.listen(config.port, () => {
console.log(`Server running in ${config.nodeEnv} mode on port ${config.port}`);
});
Real-World Example: Blog API
Let's create a simple blog API with proper file organization:
blog-api/
├── controllers/
│ ├── authController.js
│ ├── postController.js
│ └── commentController.js
├── models/
│ ├── userModel.js
│ ├── postModel.js
│ └── commentModel.js
├── routes/
│ ├── authRoutes.js
│ ├── postRoutes.js
│ └── commentRoutes.js
├── middlewares/
│ ├── authMiddleware.js
│ └── errorMiddleware.js
├── utils/
│ ├── apiFeatures.js
│ └── catchAsync.js
├── config/
│ └── index.js
├── app.js
└── server.js
In this example:
app.js
contains Express configurationserver.js
handles server startup and database connection- Routes, controllers, and models are separated by resource
Here's a simplified implementation of the post controller:
// controllers/postController.js
const Post = require('../models/postModel');
const catchAsync = require('../utils/catchAsync');
exports.getAllPosts = catchAsync(async (req, res, next) => {
const posts = await Post.find();
res.status(200).json({
status: 'success',
results: posts.length,
data: { posts }
});
});
exports.getPost = catchAsync(async (req, res, next) => {
const post = await Post.findById(req.params.id);
if (!post) {
return next(new Error('No post found with that ID'));
}
res.status(200).json({
status: 'success',
data: { post }
});
});
exports.createPost = catchAsync(async (req, res, next) => {
// Add the current user as the author
req.body.author = req.user.id;
const newPost = await Post.create(req.body);
res.status(201).json({
status: 'success',
data: { post: newPost }
});
});
And the utility function for handling async errors:
// utils/catchAsync.js
module.exports = fn => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
Summary
Proper file organization in Express.js applications is crucial for maintainability, scalability, and collaboration. Key takeaways include:
- Start with a simple structure for small projects
- Adopt MVC or feature-based organization for larger applications
- Separate concerns: routes, controllers, models, middleware
- Keep configuration separate from application code
- Be consistent with your naming conventions and structure
- Document your file organization
By following these principles, your Express applications will be easier to maintain and extend over time.
Additional Resources
Exercises
- Convert a simple Express app with all code in one file to an MVC structure
- Create a RESTful API for a resource of your choice (e.g., books, movies) using the MVC pattern
- Refactor an existing Express application to use feature-based organization
- Create a middleware directory and extract common middleware functions from your routes
- Set up environment-based configuration using the
config
pattern described above
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)