The Problem
Diana leads frontend at a 30-person company. The backend API for a new feature won't be ready for 3 weeks. Frontend waits, doing nothing. When they try to work in parallel, they build against imagined API shapes that don't match the real API. The team hardcodes JSON responses in components, which then breaks when the real API differs. They need a mock server that matches the API contract, generates realistic data, simulates errors and delays, and can be updated as the API design evolves — without touching frontend code.
Step 1: Build the Mock Server
typescript
// src/mock/server.ts — Configurable API mock server
import { Hono } from "hono";
import { cors } from "hono/cors";
import { z } from "zod";
import { watch } from "node:fs";
const app = new Hono();
app.use("*", cors());
interface MockRoute {
method: string;
path: string;
status: number;
response: any | ((req: any) => any);
delay?: number; // simulated latency in ms
errorRate?: number; // 0-1, probability of returning an error
headers?: Record<string, string>;
description?: string;
}
interface MockConfig {
routes: MockRoute[];
globalDelay?: number;
globalErrorRate?: number;
seed?: number; // for reproducible random data
}
let currentConfig: MockConfig = { routes: [] };
// Register mock routes
function applyConfig(config: MockConfig): void {
currentConfig = config;
console.log(`[Mock] Loaded ${config.routes.length} routes`);
}
// Dynamic route handler
app.all("*", async (c) => {
const method = c.req.method.toUpperCase();
const path = c.req.path;
// Find matching route
const route = currentConfig.routes.find((r) => {
if (r.method.toUpperCase() !== method) return false;
return matchPath(r.path, path);
});
if (!route) {
return c.json({
error: "No mock defined",
method,
path,
availableRoutes: currentConfig.routes.map((r) => `${r.method} ${r.path}`),
}, 404);
}
// Simulate error
const errorRate = route.errorRate ?? currentConfig.globalErrorRate ?? 0;
if (Math.random() < errorRate) {
await simulateDelay(route.delay || currentConfig.globalDelay || 0);
return c.json({
error: "Simulated server error",
message: "This error was generated by the mock server",
}, 500);
}
// Simulate delay
const delay = route.delay ?? currentConfig.globalDelay ?? 0;
if (delay > 0) {
await simulateDelay(delay + Math.random() * delay * 0.3); // ±30% jitter
}
// Generate response
const params = extractParams(route.path, path);
const query = Object.fromEntries(new URL(c.req.url).searchParams);
let body: any;
try { body = await c.req.json(); } catch { body = null; }
const response = typeof route.response === "function"
? route.response({ params, query, body, headers: Object.fromEntries(c.req.raw.headers) })
: route.response;
// Set custom headers
if (route.headers) {
for (const [key, value] of Object.entries(route.headers)) {
c.header(key, value);
}
}
c.header("X-Mock-Server", "true");
c.header("X-Mock-Delay", String(delay));
return c.json(response, route.status as any);
});
function matchPath(pattern: string, actual: string): boolean {
const patternParts = pattern.split("/");
const actualParts = actual.split("/");
if (patternParts.length !== actualParts.length) return false;
return patternParts.every((part, i) =>
part.startsWith(":") || part === actualParts[i]
);
}
function extractParams(pattern: string, actual: string): Record<string, string> {
const params: Record<string, string> = {};
const patternParts = pattern.split("/");
const actualParts = actual.split("/");
patternParts.forEach((part, i) => {
if (part.startsWith(":")) {
params[part.slice(1)] = actualParts[i];
}
});
return params;
}
function simulateDelay(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
// Fake data generators
function fakeUser(id?: string) {
const names = ["Alice Chen", "Bob Smith", "Carla Rivera", "Dan Kim", "Eva Müller"];
const idx = id ? parseInt(id) % names.length : Math.floor(Math.random() * names.length);
return {
id: id || `user-${Math.random().toString(36).slice(2, 8)}`,
name: names[idx],
email: `${names[idx].toLowerCase().replace(" ", ".")}@example.com`,
avatar: `https://i.pravatar.cc/150?u=${id || idx}`,
plan: ["free", "pro", "enterprise"][idx % 3],
createdAt: new Date(Date.now() - Math.random() * 90 * 86400000).toISOString(),
};
}
function fakeList<T>(generator: () => T, count: number): T[] {
return Array.from({ length: count }, generator);
}
// Example config
applyConfig({
globalDelay: 200,
routes: [
{
method: "GET",
path: "/api/users",
status: 200,
description: "List users with pagination",
response: ({ query }: any) => {
const page = parseInt(query.page || "1");
const limit = parseInt(query.limit || "10");
return {
users: fakeList(() => fakeUser(), limit),
pagination: { page, limit, total: 150, totalPages: Math.ceil(150 / limit) },
};
},
},
{
method: "GET",
path: "/api/users/:id",
status: 200,
response: ({ params }: any) => fakeUser(params.id),
},
{
method: "POST",
path: "/api/users",
status: 201,
response: ({ body }: any) => ({
...fakeUser(),
...body,
id: `user-${Date.now()}`,
createdAt: new Date().toISOString(),
}),
},
{
method: "DELETE",
path: "/api/users/:id",
status: 204,
response: null,
},
{
method: "GET",
path: "/api/users/:id/orders",
status: 200,
delay: 500, // simulate slow query
errorRate: 0.1, // 10% error rate
response: ({ params }: any) => ({
orders: fakeList(() => ({
id: `order-${Math.random().toString(36).slice(2, 8)}`,
amount: Math.round(Math.random() * 500 * 100) / 100,
status: ["pending", "processing", "shipped", "delivered"][Math.floor(Math.random() * 4)],
createdAt: new Date(Date.now() - Math.random() * 30 * 86400000).toISOString(),
}), 5),
}),
},
],
});
// Hot-reload mock definitions
if (process.env.MOCK_CONFIG_PATH) {
watch(process.env.MOCK_CONFIG_PATH, async () => {
try {
delete require.cache[require.resolve(process.env.MOCK_CONFIG_PATH!)];
const newConfig = require(process.env.MOCK_CONFIG_PATH!);
applyConfig(newConfig);
console.log("[Mock] Config hot-reloaded");
} catch (err) {
console.error("[Mock] Hot-reload failed:", err);
}
});
}
export default app;
Results
- Frontend starts day 1 — mock server matches the API contract; frontend builds against realistic endpoints while backend is still in development
- API contract mismatches caught early — when backend finishes, switching from mock to real API reveals mismatches in the contract immediately, not after 3 weeks of parallel work
- Error handling tested properly —
errorRate: 0.1simulates real-world failures; frontend teams build error states, retry logic, and loading states from the start - Realistic latency testing —
delay: 500simulates slow queries; developers see loading spinners and optimize perceived performance before the real API exists - Hot-reload — update mock definitions without restarting the server; API shape changes are reflected instantly during development