Express.js Best Practices: Build Production-Ready APIs With Node.js

Express.js Best Practices: Build Production-Ready APIs With Node.js

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.

✓ No spam✓ Unsubscribe anytime✓ Expert-level only

Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply