Why Express?
Express is the most widely used Node.js web framework for good reason: it's minimal, flexible, and gets out of your way. It doesn't enforce a specific project structure or architecture, which means you make the decisions — and understanding those decisions is what this guide is about.
Project Setup
Start with a clean Node.js project:
mkdir my-api && cd my-api
npm init -y
npm install express
npm install --save-dev nodemon
Add a start script to package.json:
"scripts": {
"dev": "nodemon src/index.js"
}
Basic Server Structure
A well-organized Express project separates concerns from the start:
src/
index.js ← Entry point
app.js ← Express app config
routes/
users.js ← Route definitions
controllers/
userController.js
middleware/
errorHandler.js
app.js
const express = require('express');
const app = express();
app.use(express.json()); // Parse JSON bodies
const userRoutes = require('./routes/users');
app.use('/api/users', userRoutes);
module.exports = app;
index.js
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Defining Routes
Keep routes thin — they should delegate logic to controllers:
// routes/users.js
const express = require('express');
const router = express.Router();
const { getUsers, createUser } = require('../controllers/userController');
router.get('/', getUsers);
router.post('/', createUser);
module.exports = router;
Writing Controllers
// controllers/userController.js
const users = []; // Replace with DB call in production
exports.getUsers = (req, res) => {
res.status(200).json({ success: true, data: users });
};
exports.createUser = (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ success: false, message: 'Name and email required' });
}
const newUser = { id: Date.now(), name, email };
users.push(newUser);
res.status(201).json({ success: true, data: newUser });
};
Centralized Error Handling
Avoid scattered try/catch blocks by using Express's built-in error-handling middleware (four-parameter form):
// middleware/errorHandler.js
module.exports = (err, req, res, next) => {
console.error(err.stack);
res.status(err.statusCode || 500).json({
success: false,
message: err.message || 'Internal Server Error',
});
};
Register it after all routes in app.js:
const errorHandler = require('./middleware/errorHandler');
app.use(errorHandler);
REST API Design Principles to Follow
- Use nouns for resources, not verbs:
/users, not/getUsers - Use HTTP methods correctly: GET = read, POST = create, PUT/PATCH = update, DELETE = remove
- Return consistent response shapes: always include a
successflag and adataormessagefield - Use appropriate HTTP status codes: 200, 201, 400, 401, 404, 500
- Validate input early and return clear error messages
Next Steps
Once your basic API is working, the natural progression is:
- Connect a real database (MongoDB with Mongoose, or PostgreSQL with Prisma)
- Add authentication (JWT or sessions)
- Implement request validation with a library like Zod or Joi
- Add rate limiting and helmet for basic security
- Write integration tests with Supertest
A clean, well-structured Express API isn't just easier to build — it's dramatically easier to maintain, debug, and hand off to teammates.