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. This guide focuses on Express.js best practices, with the production trade-offs senior engineers need to make sound decisions.
⚡ 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 — Express.js best practices
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); // VersioningRequest 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.
Related on CheatCoders: Node.js API security best practices, OAuth 2.0 and JWT in production, fixing Node.js event-loop blocking, and Node.js production error handling.
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.
Related: Node.js Error Handling: From try/catch to Production Error Monitoring · Prompt Engineering for Developers: Patterns That Produce Production-Ready Code Every Time · REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
Related: GraphQL vs REST: A Practical Comparison for Production APIs in 2025 · Day 97: Build vs Buy the Agent Runtime · Day 99: Capstone Build Week Checklist
Most viewed
- Python Decorators Explained: From Simple Wrappers to Production Patterns
- Agentic Git Workflows: Atomic Commits From Noisy LLM Diffs
- Python String Methods: Every str Method With Real Production Examples
- REST API Design Best Practices: The Patterns That Make APIs a Joy to Use
- AI Agent Frameworks in 2025: LangGraph vs CrewAI vs AutoGen vs Raw API
Newly added
- Amazon Bedrock Model Evaluation: Score Coding-Agent Outputs Before You Promote a Prompt
- AWS App Runner: Host Coding-Agent HTTP APIs Without Babysitting Containers
- Amazon S3 Object Lock: Immutable Artifact Buckets Coding Agents Cannot Overwrite
- AWS Security Hub: Aggregate Coding-Agent Security Findings Across Accounts
- Amazon Detective: Investigate Compromised Coding-Agent Credentials After GuardDuty
Deep-dive PDF
Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.
Already subscribed? or open the subscribe page.
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
