When you're just starting out with Express.js, it's easy to write everything in a single file like index.js
. But as your application grows, this quickly becomes hard to manage.
That’s why a clean and scalable folder structure is crucial.
In this blog post, you’ll learn:
- Why folder structure matters
- A standard way to organize an Express app
- What each folder/file does
Why Organize?
A good project structure:
- Makes your code readable
- Keeps logic separated
- Helps you scale easily
- Makes debugging and testing simpler
Basic Express Folder Structure
Here’s a recommended folder structure for a moderate-sized Express app:
my-express-app/
├── node_modules/
├── src/
│ ├── controllers/
│ ├── routes/
│ ├── models/
│ ├── middleware/
│ ├── config/
│ ├── utils/
│ └── app.js
├── .env
├── package.json
└── server.js
src/
— Main App Folder
This folder holds all your actual source code. Inside it:
controllers/
This folder contains your business logic. Each file typically handles a specific route or resource.
// src/controllers/userController.js
exports.getUsers = (req, res) => {
res.send('List of users');
};
routes/
This is where you define Express routes and connect them to controllers.
// src/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
router.get('/users', userController.getUsers);
module.exports = router;
models/
Used for database models. You might use Mongoose for MongoDB or Sequelize for SQL.
// src/models/User.js
const mongoose = require('mongoose');
const UserSchema = new mongoose.Schema({
name: String,
email: String,
});
module.exports = mongoose.model('User', UserSchema);
middleware/
Custom middleware functions like authentication, logging, error handling, etc.
// src/middleware/logger.js
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
};
module.exports = logger;
config/
Store configuration files like DB connection, environment setup, constants.
// src/config/db.js
const mongoose = require('mongoose');
const connectDB = async () => {
await mongoose.connect(process.env.MONGO_URI);
console.log('MongoDB connected');
};
module.exports = connectDB;
utils/
Helper functions and utilities (e.g., validators, formatters, token generators).
// src/utils/generateToken.js
const jwt = require('jsonwebtoken');
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '1d' });
};
module.exports = generateToken;
app.js
This is where you configure Express and mount all routes & middleware.
const express = require('express');
const userRoutes = require('./routes/userRoutes');
const logger = require('./middleware/logger');
const app = express();
app.use(express.json());
app.use(logger);
app.use('/api', userRoutes);
module.exports = app;
server.js
Entry point of the app. Responsible for starting the server.
const app = require('./src/app');
const dotenv = require('dotenv');
dotenv.config();
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
.env
Environment variables like:
PORT=5000
MONGO_URI=mongodb+srv://...
JWT_SECRET=mysecretkey
Use the dotenv
package to access these in your app.
Benefits of This Structure
- Clear separation of concerns
- Easier testing and debugging
- Scales well with team collaboration
- Ready for features like logging, authentication, APIs, etc.
Next Steps
Now that you have a solid structure, you can:
- Add authentication
- Connect a database
- Deploy to Render, Vercel, or Heroku
- Use Postman to test your APIs
TL;DR
Keep your code clean by:
- Separating files by purpose
- Moving configs and middleware to their own places
- Treating your
src
folder as the app’s core
💡 Pro tip: You don’t need to use this full structure on day one. Start simple and refactor as your app grows.
Happy coding 👨💻👩💻
Top comments (0)