Skip to main content

Express Project Structure

Introduction

A well-organized project structure is crucial for building maintainable, scalable Express applications. When you're new to Express.js, figuring out how to organize your project files can be challenging. There's no single "correct" structure, but there are established patterns that help keep code organized as your application grows.

In this guide, we'll explore common Express project structures, understand the philosophy behind different organizational approaches, and learn how to set up a project that scales well with your application's complexity.

Basic Express Project Structure

When you start a new Express application, your initial structure might be as simple as:

my-express-app/
├── node_modules/
├── app.js
├── package.json
└── package-lock.json

Let's break down this minimal setup:

  • node_modules/ - Contains all installed npm packages
  • app.js - The main application file where Express is configured
  • package.json - Contains project metadata and dependencies
  • package-lock.json - Locks down the exact versions of installed packages

Here's what a basic app.js file might look like:

javascript
const express = require('express');
const app = express();
const port = 3000;

app.get('/', (req, res) => {
res.send('Hello World!');
});

app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});

While this structure works for very simple applications, it quickly becomes unwieldy as your application grows.

Standard Express Project Structure

For more substantial applications, you'll want a structure that separates concerns and makes your code more modular. Here's a commonly used structure:

my-express-app/
├── node_modules/
├── public/
│ ├── css/
│ ├── js/
│ └── images/
├── src/
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ ├── middlewares/
│ ├── utils/
│ └── app.js
├── views/
├── tests/
├── package.json
└── package-lock.json

Key Directories Explained

1. public/

The public directory contains static assets that are directly served to the client:

javascript
// In app.js
app.use(express.static('public'));

With this configuration, a file at public/css/style.css would be accessible at http://yourapp.com/css/style.css.

2. src/

The src directory contains your application's source code, organized into subdirectories:

  • controllers/ - Handle the request/response logic
  • models/ - Define data models, often used with databases
  • routes/ - Define URL routes and connect them to controllers
  • middlewares/ - Custom middleware functions
  • utils/ - Utility/helper functions
  • app.js - Main application setup

3. views/

If your application serves HTML using a template engine like EJS, Pug, or Handlebars, the templates go here:

javascript
// In app.js
app.set('views', './views');
app.set('view engine', 'ejs');

4. tests/

Contains test files for your application.

Practical Example: File Organization

Let's see how we might structure a simple blog application:

1. First, set up the main app.js file:

javascript
// src/app.js
const express = require('express');
const morgan = require('morgan');
const path = require('path');

// Import routes
const postRoutes = require('./routes/posts');
const userRoutes = require('./routes/users');

// Initialize express
const app = express();

// Middlewares
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, '../public')));

// Setup view engine
app.set('views', path.join(__dirname, '../views'));
app.set('view engine', 'ejs');

// Routes
app.use('/posts', postRoutes);
app.use('/users', userRoutes);

// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

module.exports = app;

2. Define routes in separate files:

javascript
// src/routes/posts.js
const express = require('express');
const router = express.Router();
const postController = require('../controllers/postController');

router.get('/', postController.getAllPosts);
router.get('/:id', postController.getPostById);
router.post('/', postController.createPost);
router.put('/:id', postController.updatePost);
router.delete('/:id', postController.deletePost);

module.exports = router;

3. Create controllers to handle the logic:

javascript
// src/controllers/postController.js
const Post = require('../models/Post');

exports.getAllPosts = async (req, res) => {
try {
const posts = await Post.find();
res.status(200).json(posts);
} catch (err) {
res.status(500).json({ message: err.message });
}
};

exports.getPostById = async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) return res.status(404).json({ message: 'Post not found' });
res.status(200).json(post);
} catch (err) {
res.status(500).json({ message: err.message });
}
};

// Other controller methods: createPost, updatePost, deletePost...

4. Define models for your data:

javascript
// src/models/Post.js
const mongoose = require('mongoose');

const postSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
createdAt: {
type: Date,
default: Date.now
}
});

module.exports = mongoose.model('Post', postSchema);

Advanced Project Structure

As applications grow, you might consider more advanced structures:

Feature-Based Structure

Organize by feature rather than function type:

src/
├── features/
│ ├── posts/
│ │ ├── postModel.js
│ │ ├── postController.js
│ │ ├── postRoutes.js
│ │ └── postService.js
│ ├── users/
│ │ ├── userModel.js
│ │ ├── userController.js
│ │ ├── userRoutes.js
│ │ └── userService.js
│ └── auth/
│ ├── authController.js
│ ├── authRoutes.js
│ └── authService.js
├── middlewares/
├── utils/
└── app.js

MVC Architecture

Model-View-Controller is a common pattern:

src/
├── models/ # Data models
├── views/ # Templates
├── controllers/ # Request handlers
├── routes/ # URL definitions
├── services/ # Business logic
├── middlewares/ # Custom middlewares
├── utils/ # Utility functions
└── app.js # Application entry point

Best Practices for Project Structure

  1. Separation of Concerns: Each file should have a single responsibility.

  2. Modularize Routes: Keep route definitions separate from their implementation.

  3. Environment Configuration: Store configuration in environment variables.

    javascript
    // config.js
    module.exports = {
    port: process.env.PORT || 3000,
    mongoURI: process.env.MONGO_URI || 'mongodb://localhost:27017/myapp',
    jwtSecret: process.env.JWT_SECRET || 'your_jwt_secret'
    };
  4. Centralized Error Handling: Create custom error middleware.

    javascript
    // src/middlewares/errorHandler.js
    function errorHandler(err, req, res, next) {
    console.error(err.stack);

    const statusCode = err.statusCode || 500;

    res.status(statusCode).json({
    success: false,
    error: {
    message: err.message || 'Internal Server Error',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
    }
    });
    }

    module.exports = errorHandler;
  5. Consistent Naming Conventions: Choose a naming style (camelCase, snake_case, etc.) and stick with it.

Starting Template for New Express Projects

Here's a starter script to generate a basic Express project structure:

javascript
// setup-project.js
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

// Directories to create
const dirs = [
'public',
'public/css',
'public/js',
'public/images',
'src',
'src/controllers',
'src/models',
'src/routes',
'src/middlewares',
'src/utils',
'views',
'tests'
];

// Create directories
dirs.forEach(dir => {
fs.mkdirSync(dir, { recursive: true });
console.log(`Created directory: ${dir}`);
});

// Create basic app.js
const appContent = `
const express = require('express');
const path = require('path');

const app = express();
const port = process.env.PORT || 3000;

// Middlewares
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, '../public')));

// Routes
app.get('/', (req, res) => {
res.send('Express app is running!');
});

app.listen(port, () => {
console.log(\`Server running at http://localhost:\${port}\`);
});

module.exports = app;
`;

fs.writeFileSync('src/app.js', appContent.trim());
console.log('Created src/app.js');

// Initialize npm and install dependencies
execSync('npm init -y');
execSync('npm install express');
execSync('npm install --save-dev nodemon');

// Update package.json with start scripts
const packageJson = JSON.parse(fs.readFileSync('package.json'));
packageJson.scripts = {
...packageJson.scripts,
start: 'node src/app.js',
dev: 'nodemon src/app.js'
};

fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
console.log('Updated package.json with start scripts');

console.log('Express project structure created successfully!');

Save this as setup-project.js and run it with Node.js:

node setup-project.js

Summary

A well-organized project structure is essential for building maintainable Express applications. While there's no one-size-fits-all approach, the structures outlined in this guide provide solid foundations that you can adapt to your needs.

Key takeaways:

  1. Start with a structure that separates concerns
  2. Organize files by function (MVC) or by feature
  3. Keep your entry point clean by delegating to modules
  4. Consider scalability as your application grows
  5. Maintain consistent naming and organizational patterns

Remember, the best project structure is one that helps your team work efficiently and makes your codebase easy to understand and extend.

Additional Resources

Exercises

  1. Create a basic Express project structure using the template provided above.
  2. Refactor an existing Express application to follow the MVC pattern.
  3. Implement a feature-based structure for a blog application with posts, comments, and users.
  4. Create a custom middleware and integrate it into the application structure.
  5. Design and implement a centralized error-handling system for an Express application.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)