Express.js apps start simple and scale into unmaintainable spaghetti — unless you structure them right from the beginning. Middleware ordering, proper error handling, request validation, security headers, compression, and graceful shutdown are the foundations that turn a toy API into a production service.
⚡ TL;DR: Middleware order matters: parse body → auth → rate limit → routes → error handler. Always validate with Zod/Joi before business logic. Use helmet for security headers. Compress responses. Structure routes by version and domain. Graceful shutdown on SIGTERM. Health check at /health.
Application structure and middleware order
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const app = express();
// 1. Security headers (first!)
app.use(helmet());
// 2. Compression
app.use(compression());
// 3. Request ID for tracing
app.use((req,res,next)=>{ req.id=crypto.randomUUID(); next(); });
// 4. Body parsing
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// 5. Rate limiting
app.use('/api/', rateLimit({ windowMs:60000, max:100 }));
// 6. Authentication
app.use('/api/', authenticateToken);
// 7. Routes
app.use('/api/v1/users', userRoutes);
app.use('/api/v1/posts', postRoutes);
// 8. 404 handler
app.use((req,res)=>res.status(404).json({error:'Not found'}));
// 9. Error handler LAST (4 params!)
app.use(errorHandler);
Router structure by domain
// routes/users.js
const router = express.Router();
// Middleware specific to this router:
router.use(requireAuth);
// CRUD routes:
router.get('/', asyncHandler(UserController.list));
router.post('/', validate(CreateUserDto), asyncHandler(UserController.create));
router.get('/:id', asyncHandler(UserController.get));
router.patch('/:id', validate(UpdateUserDto), asyncHandler(UserController.update));
router.delete('/:id', asyncHandler(UserController.delete));
module.exports = router;
// app.js registers it:
app.use('/api/v1/users', userRoutes);
app.use('/api/v2/users', userRoutesV2); // Versioning
Request validation middleware
const { z } = require('zod');
const validate = schema => async (req,res,next) => {
try {
req.body = await schema.parseAsync(req.body);
next();
} catch(err) {
if(err instanceof z.ZodError) {
return res.status(422).json({
error: 'Validation failed',
fields: err.errors.map(e=>({path:e.path.join('.'),message:e.message}))
});
}
next(err);
}
};
const CreateUserDto = z.object({
name: z.string().min(1).max(100).trim(),
email: z.string().email().toLowerCase(),
age: z.number().int().min(18).max(120)
});
router.post('/', validate(CreateUserDto), asyncHandler(UserController.create));
Health check and graceful shutdown
// Health check — returns 200 if healthy, 503 if degraded
app.get('/health', async (req,res) => {
const checks = await Promise.allSettled([
db.query('SELECT 1'),
redis.ping(),
]);
const healthy = checks.every(c=>c.status==='fulfilled');
res.status(healthy?200:503).json({
status: healthy?'healthy':'degraded',
timestamp: new Date().toISOString()
});
});
// Graceful shutdown:
const server = app.listen(process.env.PORT||3000);
process.on('SIGTERM',()=>{
server.close(async()=>{
await db.end();
await redis.quit();
process.exit(0);
});
setTimeout(()=>process.exit(1),30000);
});
- ✅ Security headers first with helmet()
- ✅ Compress responses (saves 60-80% bandwidth)
- ✅ Validate ALL inputs before business logic
- ✅ Router files per domain — not one giant routes file
- ✅ Health check endpoint for load balancer probes
- ❌ Middleware order matters — error handler must be last
- ❌ Never skip validation — assume all input is adversarial
External reference: Express.js production best practices.
Recommended Reading
→ Designing Data-Intensive Applications — The bible of distributed systems and production engineering at scale.
→ The Pragmatic Programmer — Timeless engineering wisdom every senior developer needs.
Affiliate links. We earn a small commission at no extra cost to you.
Free Weekly Newsletter
🚀 Join 2,000+ Senior Developers
Get expert-level JavaScript, Python, AWS, system design and AI secrets every week. Zero fluff, pure signal.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
