Project Structure
src/
routes/
controllers/
middleware/
models/
services/
app.js
server.js
Setting Up Express
// app.js
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const app = express();
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGIN }));
app.use(express.json());
app.use('/api/products', require('./routes/products'));
module.exports = app;
Defining Routes and Controllers
// routes/products.js
const router = require('express').Router();
const controller = require('../controllers/productController');
router.get('/', controller.list);
router.get('/:id', controller.getOne);
router.post('/', controller.create);
router.put('/:id', controller.update);
router.delete('/:id', controller.remove);
module.exports = router;
// controllers/productController.js
exports.list = async (req, res, next) => {
try {
const products = await ProductService.findAll(req.query);
res.json(products);
} catch (err) {
next(err);
}
};
exports.create = async (req, res, next) => {
try {
const product = await ProductService.create(req.body);
res.status(201).json(product);
} catch (err) {
next(err);
}
};
Centralized Error Handling
// middleware/errorHandler.js
function errorHandler(err, req, res, next) {
const status = err.statusCode || 500;
res.status(status).json({
error: err.message || 'Internal server error',
});
}
module.exports = errorHandler;
// app.js (added at the end, after routes)
app.use(require('./middleware/errorHandler'));
Input Validation
const { z } = require('zod');
const productSchema = z.object({
name: z.string().min(1),
price: z.number().positive(),
});
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) return res.status(400).json(result.error);
req.body = result.data;
next();
};
}
router.post('/', validate(productSchema), controller.create);
Pagination
exports.list = async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const products = await Product.find()
.skip((page - 1) * limit)
.limit(limit);
res.json({ data: products, page, limit });
};
Conclusion
A clean Express API comes down to consistent structure: routes stay thin, controllers handle request/response shaping, services hold business logic, and a single error-handling middleware catches everything. This separation keeps the codebase testable and predictable as it grows.