Production-Ready REST API (Express)
Error handling, Zod validation, rate limiting, CORS, and structured logging
What You’ll Build
After following this guide, you will have a working implementation of production-ready rest api (express) in your project. Build a production-quality REST API with Express.js. Goes beyond basic CRUD to include input validation with Zod, centralized error handling, CORS configuration, rate limiting, and structured JSON logging. This is the foundation every serious API needs.
Use Cases & Problems Solved
- Build reliable server endpoints that clients can consume consistently
- Handle common backend patterns like routing, middleware, and error handling
- Provide a clean interface between your frontend and data layer
Prerequisites
- Node.js 18+
- Express.js
Step-by-Step Implementation
Install core dependencies
The following snippet shows how to install core dependencies. Copy this into your project and adjust the values for your environment.
npm install express zod cors helmet express-rate-limit pino pino-http
App setup with middleware
The following snippet shows how to app setup with middleware. Copy this into your project and adjust the values for your environment.
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const pinoHttp = require('pino-http');
const app = express();
app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || '*' }));
app.use(express.json({ limit: '10kb' }));
app.use(pinoHttp());
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
Zod validation middleware
The following snippet shows how to zod validation middleware. Copy this into your project and adjust the values for your environment.
const { z } = require('zod');
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
details: result.error.issues.map(i => ({ path: i.path, message: i.message })),
});
}
req.body = result.data; // Use parsed/sanitized data
next();
};
}
const createUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
age: z.number().int().min(13).optional(),
});
app.post('/api/users', validate(createUserSchema), createUser);
Centralized error handler
The following snippet shows how to centralized error handler. Copy this into your project and adjust the values for your environment.
// Must be LAST middleware — catches all unhandled errors
app.use((err, req, res, next) => {
req.log.error(err);
const status = err.status || 500;
res.status(status).json({
error: status === 500 ? 'Internal server error' : err.message,
...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
});
});
⚠️ Don’t Do This
❌ Exposing stack traces and internal errors to clients
app.get('/api/data', async (req, res) => {
try { ... } catch (err) {
res.status(500).json({ error: err.message, stack: err.stack });
// Leaks internal paths, DB schema, etc!
}
});
✅ Return generic errors in production, detailed in development
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message,
});
Testing
Add these tests to verify your API endpoints work correctly:
// __tests__/api.test.ts
import { describe, it, expect } from 'vitest';
describe('Production-Ready REST API (Express)', () => {
it('should return 200 for valid requests', async () => {
const res = await fetch('/api/endpoint', { method: 'GET' });
expect(res.status).toBe(200);
const data = await res.json();
expect(data).toBeDefined();
});
it('should return 400 for invalid input', async () => {
const res = await fetch('/api/endpoint', {
method: 'POST',
body: JSON.stringify({}),
});
expect(res.status).toBe(400);
});
it('should handle errors gracefully', async () => {
const res = await fetch('/api/endpoint/nonexistent');
expect(res.status).toBe(404);
});
});
Verification
# Start server and test validation:
curl -X POST http://localhost:3000/api/users \
-H 'Content-Type: application/json' \
-d '{"email":"not-an-email","name":"A"}'
# Should get 400 with validation errors Related Specs
Secure Webhook Handler
Signature verification, idempotency, retry handling, and queue processing
Real-Time Updates with SSE
EventSource API, reconnection, broadcasting, and Express/Fastify SSE
GraphQL API with Apollo Server
Schema, resolvers, context, dataloaders, and error handling