TrueSpec

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

Advanced

Secure Webhook Handler

Signature verification, idempotency, retry handling, and queue processing

API & Backend
Intermediate

Real-Time Updates with SSE

EventSource API, reconnection, broadcasting, and Express/Fastify SSE

API & Backend
Intermediate

GraphQL API with Apollo Server

Schema, resolvers, context, dataloaders, and error handling

API & Backend