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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)