If you've been working with Node.js and Express for a while, you've probably realized that as your projects grow, keeping everything organized becomes a real challenge. I learned this the hard way when my first big Express project turned into a complete mess of spaghetti code that even I couldn't understand after a few months.
After years of trial and error (and many late-night refactoring sessions), I've finally settled on a folder structure that actually makes sense and keeps my sanity intact. In this post, I'll walk you through what I consider to be a solid structure for Express.js apps and explain why each piece matters.
The structure
Before diving into details, here's what a well-organized Express app structure typically looks like:
š
āāā š app.js
āāā š bin
āāā š config
āāā š controllers
ā āāā š customer.js
ā āāā š product.js
āāā š middleware
ā āāā š auth.js
ā āāā š logger.js
āāā š models
ā āāā š customer.js
ā āāā š product.js
āāā š routes
ā āāā š api.js
ā āāā š auth.js
āāā š public
ā āāā š css
ā āāā š js
ā āāā š images
āāā š views
ā āāā š index.ejs
ā āāā š product.ejs
āāā š tests
ā āāā š unit
ā āāā š integration
ā āāā š e2e
āāā š utils
ā āāā š validation.js
ā āāā š helpers.js
āāā š node_modules
Now, let's break down what each of these folders and files actually does.
Core files
The most important files that start and set up your Express app. These are like the engine and control panel of your app - without them, nothing else works.
App.js - Your application's starting point
Think of app.js
as the heart of your Express application. This is where everything comes together - you set up your Express instance, connect your middleware, hook up your routes, and get the server running.
const express = require('express');
const app = express();
const config = require('./config');
const routes = require('./routes');
// Set up middleware
app.use(express.json());
// Hook up routes
app.use('/api', routes);
// Fire up the server
const PORT = config.port || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;
I've seen developers overcomplicate this file with too much logic, but it's best to keep it clean and focused on initialization.
Bin - Your server scripts
The bin
directory is where you put scripts related to starting your server. The most common file here is www
which handles server creation and error handling. This separation helps keep your core application logic (in app.js
) distinct from server-specific concerns.
#!/usr/bin/env node
const app = require('../app');
const debug = require('debug')('your-app:server');
const http = require('http');
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
const server = http.createServer(app);
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
// Helper functions
function normalizePort(val) {
const port = parseInt(val, 10);
if (isNaN(port)) return val;
if (port >= 0) return port;
return false;
}
function onError(error) {
if (error.syscall !== 'listen') throw error;
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
// Handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
function onListening() {
const addr = server.address();
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
debug('Listening on ' + bind);
}
Config - Your application settings
The config
directory is where all your environment-specific settings live. This includes database connection strings, API keys, and other configuration variables. Keeping these in a separate folder makes it easy to switch between development, staging, and production environments.
module.exports = {
port: process.env.PORT || 3000,
db: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 27017,
name: process.env.DB_NAME || 'mydatabase'
},
jwt: {
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: '1d'
}
};
Main workers
The heavy-lifting components that handle your business logic. These folders contain the code that processes requests, manages data, and implements your app's core functionality.
Controllers - Your request handlers
Controllers contain the logic that processes incoming requests and sends back responses. I like to organize controllers by resource or feature - each file handles operations related to a specific part of the application.
const Customer = require('../models/customer');
exports.getAllCustomers = async (req, res) => {
try {
const customers = await Customer.find();
res.json(customers);
} catch (err) {
res.status(500).json({ message: "Couldn't fetch customers", error: err.message });
}
};
exports.getCustomerById = async (req, res) => {
try {
const customer = await Customer.findById(req.params.id);
if (!customer) return res.status(404).json({ message: 'Customer not found' });
res.json(customer);
} catch (err) {
res.status(500).json({ message: "Couldn't fetch customer", error: err.message });
}
};
Middleware - Your request processors
Middleware functions handle tasks that should happen before (or sometimes after) your main controller logic. Common examples include authentication, logging, and input validation.
// auth.js - JWT authentication middleware
const jwt = require('jsonwebtoken');
const config = require('../config');
module.exports = (req, res, next) => {
// Get token from header
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ message: 'No token provided, access denied' });
}
try {
// Verify the token
const decoded = jwt.verify(token, config.jwt.secret);
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ message: 'Invalid token, access denied' });
}
};
Models - Your data structures
Models define the structure of your data and handle database interactions. If you're using MongoDB with Mongoose, this is where your schemas live. For SQL databases with Sequelize, your model definitions go here.
const mongoose = require('mongoose');
const customerSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true
},
email: {
type: String,
required: [true, 'Email is required'],
unique: true,
lowercase: true,
trim: true
},
phone: {
type: String,
trim: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('Customer', customerSchema);
Routes - Your URL mappings
Routes connect URLs to controller functions. They define what happens when a user visits a specific endpoint. I usually organize routes by resource or API version.
const express = require('express');
const router = express.Router();
const customerController = require('../controllers/customer');
const authMiddleware = require('../middleware/auth');
// Public routes
router.get('/customers', customerController.getAllCustomers);
router.get('/customers/:id', customerController.getCustomerById);
// Protected routes
router.post('/customers', authMiddleware, customerController.createCustomer);
router.put('/customers/:id', authMiddleware, customerController.updateCustomer);
router.delete('/customers/:id', authMiddleware, customerController.deleteCustomer);
module.exports = router;
The frontend stuff
Everything your users see and interact with directly. This includes static files like stylesheets and images, plus the templates that generate your HTML pages.
Public - Your static assets
The public
directory contains files that are served directly to the client without any processing - CSS, client-side JavaScript, images, fonts, etc.
Views - Your HTML templates
If you're rendering HTML on the server (using a templating engine like EJS, Pug, or Handlebars), your templates go in the views
directory.
<!-- views/index.ejs -->
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/css/styles.css">
</head>
<body>
<header>
<h1><%= title %></h1>
</header>
<main>
<% if (customers.length > 0) { %>
<ul class="customer-list">
<% customers.forEach(customer => { %>
<li>
<h3><%= customer.name %></h3>
<p><%= customer.email %></p>
</li>
<% }); %>
</ul>
<% } else { %>
<p>No customers found.</p>
<% } %>
</main>
<script src="/js/main.js"></script>
</body>
</html>
Support files
The behind-the-scenes helpers that keep your app running smoothly. These include testing suites, utility functions, and external dependencies that support your main application code.
Tests - Your test files
All your tests go in the tests
directory. I like to organize them into subdirectories:
unit
for testing individual functions and componentsintegration
for testing how different parts work togethere2e
(end-to-end) for testing complete user flows
Utils - Your helper functions
The utils
directory contains reusable utility functions and helpers that don't fit neatly into the other categories.
// utils/validation.js
exports.isValidEmail = (email) => {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(String(email).toLowerCase());
};
exports.isStrongPassword = (password) => {
// At least 8 chars, one uppercase, one lowercase, one number, one special char
const re = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
return re.test(password);
};
node_modules - Your dependencies
This is where npm (or yarn) installs all the packages your project depends on. You should never modify files in this directory directly - that's what package.json
is for.
Real-World Tips
After building dozens of Express apps, I've learned a few things:
Don't overengineer - Start simple and add complexity only when needed. Not every app needs every folder.
Be consistent - Once you choose a structure, stick with it. Consistency makes your codebase easier to navigate.
Use descriptive names - Name your files based on what they do, not what they are. For example,
userAuthentication.js
is better thanauth.js
.Keep related files close - If you have a file that's only used by one controller, consider keeping it in a subdirectory with that controller rather than in a separate utility folder.
Document your decisions - Add a README.md explaining your folder structure for new team members.
Conclusion
A good Express.js application structure isn't about following a rigid template ā it's about organizing your code in a way that makes sense for your specific project and team. The structure I've outlined here has worked well for me across many projects, but don't be afraid to adapt it to your needs. Remember, the goal is to create an organization system that helps you build and maintain your application more effectively, not to follow someone else's rules. As your project grows, you'll thank yourself for taking the time to set up a clean, logical structure from the beginning.