Skip to main content

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:

  1. First, create your project and install Express:
bash
mkdir my-express-app
cd my-express-app
npm init -y
npm install express
  1. Create your main app.js file:
javascript
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:

javascript
// 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:

javascript
// 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:

  1. Create a model:
javascript
// 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);
  1. Create a controller:
javascript
// 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
});
}
};
  1. Set up routes:
javascript
// 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;
  1. Connect everything in app.js:
javascript
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:

  1. Be consistent: Once you choose a pattern, stick with it throughout the application
  2. Keep files focused: Each file should have a single responsibility
  3. Use descriptive names: Name files and folders based on what they do, not how they do it
  4. Separate configuration: Keep configuration parameters in dedicated files
  5. Group middleware: Store custom middleware functions in their own directory
  6. Create utility functions: Extract common functionality into helper/utility files
  7. 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:

javascript
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:

javascript
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 configuration
  • server.js handles server startup and database connection
  • Routes, controllers, and models are separated by resource

Here's a simplified implementation of the post controller:

javascript
// 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:

javascript
// 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

  1. Convert a simple Express app with all code in one file to an MVC structure
  2. Create a RESTful API for a resource of your choice (e.g., books, movies) using the MVC pattern
  3. Refactor an existing Express application to use feature-based organization
  4. Create a middleware directory and extract common middleware functions from your routes
  5. 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! :)