---
title: 'Mock Servers'
source: 'https://academia.sh/en/courses/api-design/mock-servers'
course: 'Web API Design'
language: en
updated: '2026-08-19T05:19:27+00:00'
license: 'CC BY-SA 4.0'
---

# Mock Servers

Writing a server that produces sample responses from a schema, developing the client against it, measuring that the same code works once it moves to the real server, and counting the cases the mock server hides.

Expectation files have been used one-directionally so far: the consumer wrote, the
provider tested. The same contract also does its job in the reverse direction. When a new
consumer starts being developed, the provider may not have written that endpoint yet; but
the consumer waiting is unnecessary, because the contract is already known.

A **mock server** turns the contract into something executable: it produces sample
responses from the schema in the definition and stands in for the real server. This lesson
writes such a server, develops a client against it, then connects the same code to the
real server and measures two things — whether the response shape is the same, and which
of the client's branches never ran.

## Adding Examples to the Schema

Producing an example is not possible for every field. Type information is enough to
produce a string, but not enough to produce a string that matches the `^U-\d{4}$` pattern.
That is why fields with a pattern have to carry an example; JSON Schema defines the
`examples` keyword for this.

```js
// definition.mjs — the loan service's machine-readable definition (with example values)
// Every field that has a pattern carries an example; the mock server uses them.
const LOAN_REQUEST = {
  type: "object", required: ["member", "items"], additionalProperties: false,
  properties: {
    member: { type: "string", pattern: "^U-\\d{4}$", examples: ["U-1001"] },
    items: { type: "array", items: { type: "object", required: ["isbn"], additionalProperties: false,
      properties: { isbn: { type: "string", pattern: "^97[89]-\\d{10}$", examples: ["978-0262033848"] } } } },
    branch: { type: "string", enum: ["central", "shore", "hill"] },
  },
};

const LOAN_RESPONSE = {
  type: "object", required: ["id", "member", "items", "returnDate", "status"], additionalProperties: false,
  properties: {
    id: { type: "string", pattern: "^O-\\d+$", examples: ["O-1"] },
    member: { type: "string", pattern: "^U-\\d{4}$", examples: ["U-1001"] },
    items: { type: "array", items: { type: "object", required: ["isbn"], additionalProperties: false,
      properties: { isbn: { type: "string", pattern: "^97[89]-\\d{10}$", examples: ["978-0262033848"] } } } },
    returnDate: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$", examples: ["2026-04-15"] },
    status: { type: "string", enum: ["open", "closed"] },
  },
};

const PROBLEM = {
  type: "object", required: ["type", "title", "status", "detail", "instance"],
  properties: {
    type: { type: "string", examples: ["https://example.library/problems/example"] },
    title: { type: "string", examples: ["Example problem"] },
    status: { type: "integer", examples: [0] },
    detail: { type: "string", examples: ["Example detail."] },
    instance: { type: "string", examples: ["oc-0001"] },
    errors: { type: "array", items: { type: "object", required: ["path", "code"],
      properties: { path: { type: "string", examples: ["/member"] }, code: { type: "string", examples: ["format"] } } } },
  },
};

export const DEFINITION = {
  "POST /loans": { request: LOAN_REQUEST, response: { 201: LOAN_RESPONSE, 422: PROBLEM } },
  "GET /loans/{id}": { request: null, response: { 200: LOAN_RESPONSE, 404: PROBLEM } },
};
```

```js
// example.mjs — produces an example value from a schema
// Rule: enum uses the first value, examples uses the first example, a pattern requires an example.
export function makeExample(schema, path = "") {
  if (schema.enum) return schema.enum[0];
  if (schema.examples?.length) return schema.examples[0];
  if (schema.pattern) throw new Error(`${path || "/"}: no examples written for a field with a pattern`);
  switch (schema.type) {
    case "string":  return "text";
    case "integer": case "number": return 0;
    case "boolean": return false;
    case "array":   return schema.items ? [makeExample(schema.items, `${path}/0`)] : [];
    case "object": {
      const obj = {};
      for (const [name, sub] of Object.entries(schema.properties ?? {})) obj[name] = makeExample(sub, `${path}/${name}`);
      return obj;
    }
    default: return null;
  }
}
```

When there is a pattern but no example, the generator errors. This is a new condition
placed on the contract: **the schema must be complete enough that an example can be
produced from it.**

```js
// missing-example.mjs — a patterned field without an example does not let the mock server start
import { makeExample } from "./example.mjs";

const COMPLETE = { type: "object", required: ["barcode"],
  properties: { barcode: { type: "string", pattern: "^BK-\\d{6}$", examples: ["BK-004312"] } } };
const INCOMPLETE = { type: "object", required: ["barcode"],
  properties: { barcode: { type: "string", pattern: "^BK-\\d{6}$" } } };

console.log("schema with example ->", JSON.stringify(makeExample(COMPLETE)));
try { makeExample(INCOMPLETE); } catch (h) { console.log("schema without example ->", h.message); }
```

```
schema with example -> {"barcode":"BK-004312"}
schema without example -> /barcode: no examples written for a field with a pattern
```

## The Mock Server

The mock server recognizes the paths in the definition, produces the examples **once**,
and returns them. Because examples are produced up front, a missing-example problem shows
up while the server is starting, not on the first request.

```js
// mock.mjs — server that produces sample responses from the definition
// Usage: node mock.mjs <port> [problem]   "problem": also produces error paths
import { createServer } from "node:http";
import { DEFINITION } from "./definition.mjs";
import { makeExample } from "./example.mjs";

const PORT = Number(process.argv[2] ?? 8439);
const PROBLEM_MODE = process.argv[3] === "problem";

// Examples are produced once; a field with a pattern but no example errors here.
const EXAMPLES = Object.fromEntries(
  Object.entries(DEFINITION).flatMap(([a, t]) =>
    Object.entries(t.response).map(([k, s]) => [`${a} ${k}`, makeExample(s)])));

const readBody = (req) =>
  new Promise((resolve) => { let v = ""; req.on("data", (p) => (v += p)); req.on("end", () => resolve(v)); });

createServer(async (req, res) => {
  res.sendDate = false;
  const path = req.url.split("?")[0];
  const key = req.method === "POST" && path === "/loans" ? "POST /loans"
    : req.method === "GET" && /^\/loans\/[^/]+$/.test(path) ? "GET /loans/{id}" : null;

  if (!key) { res.writeHead(404).end(); return; }
  await readBody(req);

  // Which status code to produce: success, or an error code in problem mode.
  const codes = Object.keys(DEFINITION[key].response).map(Number);
  const success = codes.find((k) => k < 400);
  const code = PROBLEM_MODE && req.headers["example-problem"] ? Number(req.headers["example-problem"]) : success;

  const body = structuredClone(EXAMPLES[`${key} ${code}`]);
  if (body && code >= 400) body.status = code;          // the real code is written into the example body
  res.writeHead(code, { "content-type": code >= 400 ? "application/problem+json; charset=utf-8" : "application/json; charset=utf-8" });
  res.end(JSON.stringify(body));
}).listen(PORT, "127.0.0.1", () => console.log(`mock server 127.0.0.1:${PORT}${PROBLEM_MODE ? " (problem mode)" : ""}`));
```

The client receives the server address from outside. The switch between the mock and the
real server happens by changing this single dependency; nothing else changes.

```js
// client.mjs — client that shows a loan record; the server address is supplied externally
export async function showLoan(base, id, extraHeaders = {}) {
  const response = await fetch(`${base}/loans/${id}`, { headers: extraHeaders });
  const body = await response.json();
  const fields = Object.keys(body).join(",");
  if (response.status === 200)
    return { case: "success", fields, line: `${body.id} · ${body.member} · ${body.items.length} items · ${body.returnDate} · ${body.status}` };
  if (response.status === 404) return { case: "not-found", fields, line: body.detail };
  if (response.status === 422) return { case: "validation", fields, line: body.errors.map((h) => `${h.path}=${h.code}`).join(", ") };
  return { case: "unknown", fields, line: `unexpected code ${response.status}` };
}
```

```js
// real.mjs — real server that conforms to the definition
import { createServer } from "node:http";

const RECORDS = new Map([["O-1", { id: "O-1", member: "U-1001", items: [{ isbn: "978-0201896831" }], returnDate: "2026-05-02", status: "closed" }]]);

createServer((req, res) => {
  res.sendDate = false;
  const id = req.url.split("?")[0].replace("/loans/", "");
  const record = RECORDS.get(id);
  if (record) {
    res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
    return res.end(JSON.stringify(record));
  }
  res.writeHead(404, { "content-type": "application/problem+json; charset=utf-8" });
  res.end(JSON.stringify({ type: "https://example.library/problems/resource-not-found", title: "Resource not found",
    status: 404, detail: `Loan record ${id} does not exist.`, instance: "oc-0007" }));
}).listen(8440, "127.0.0.1", () => console.log("real server 127.0.0.1:8440"));
```

## Same Code, Three Servers

The runner below runs the client against a given address, writes the returned body's field
set, and counts how many distinct branches of the client ran.

```js
// run.mjs — runs the same client code against a given address, reports which cases
// ran and the shape of the response.
// Usage: node run.mjs <base-address> [problem]
import { showLoan } from "./client.mjs";

const BASE = process.argv[2];
const PROBLEM = process.argv[3] === "problem";

const CASES = PROBLEM
  ? [["existing record", "O-1", {}], ["missing record", "O-9", { "example-problem": "404" }]]
  : [["existing record", "O-1", {}], ["missing record", "O-9", {}]];

const cases = new Set();
for (const [name, id, headers] of CASES) {
  const s = await showLoan(BASE, id, headers);
  cases.add(s.case);
  console.log(`${name.padEnd(16)} case=${s.case.padEnd(12)} ${s.line}`);
  console.log(`${"".padEnd(16)} fields: ${s.fields}`);
}
console.log(`distinct cases run: ${cases.size}/4  (${[...cases].join(", ")})`);
```

```bash
#!/usr/bin/env bash
# Same client code: first against the mock server, then against the real server.
node mock.mjs 8439 & mock=$!
node real.mjs & real=$!
sleep 0.6

echo "== mock server (success examples only) =="
node run.mjs http://127.0.0.1:8439
kill "$mock"; wait "$mock" 2>/dev/null

node mock.mjs 8439 problem & mock=$!
sleep 0.5
echo "== mock server (problem mode) =="
node run.mjs http://127.0.0.1:8439 problem
kill "$mock"; wait "$mock" 2>/dev/null

echo "== real server =="
node run.mjs http://127.0.0.1:8440
kill "$real"; wait "$real" 2>/dev/null
```

```
real server 127.0.0.1:8440
mock server 127.0.0.1:8439
== mock server (success examples only) ==
existing record  case=success      O-1 · U-1001 · 1 items · 2026-04-15 · open
                 fields: id,member,items,returnDate,status
missing record   case=success      O-1 · U-1001 · 1 items · 2026-04-15 · open
                 fields: id,member,items,returnDate,status
distinct cases run: 1/4  (success)
mock server 127.0.0.1:8439 (problem mode)
== mock server (problem mode) ==
existing record  case=success      O-1 · U-1001 · 1 items · 2026-04-15 · open
                 fields: id,member,items,returnDate,status
missing record   case=not-found    Example detail.
                 fields: type,title,status,detail,instance,errors
distinct cases run: 2/4  (success, not-found)
== real server ==
existing record  case=success      O-1 · U-1001 · 1 items · 2026-05-02 · closed
                 fields: id,member,items,returnDate,status
missing record   case=not-found    Loan record O-9 does not exist.
                 fields: type,title,status,detail,instance
distinct cases run: 2/4  (success, not-found)
```

The third section confirms the actual claim: client code developed against the mock
server worked **unchanged** once connected to the real server. The success body's field
set is identical across both servers; the values differ, the shape does not. The only
thing that changed to move the client to the real server was the address.

The first section shows the mock server's trap. A mock server that only produces success
examples returns 200 even when you ask for a record that does not exist. Only one of the
client's four branches runs, and the developer considers the job done without ever seeing
the error paths. This is the most common cause of breakage in the move to the real server:
what is missing is not the body shape but an error branch that was never written.

The second section gives the solution. When the mock server can learn which status code
to produce from the request, all of the client's paths can be run during development. The
count of branches run went from one to two, reaching the same number as the real server.
Being able to request every status code defined in the contract from the mock server
turns it from a demo tool into a development tool.

## The Mock Server's Generosity

The field sets of the 404 bodies in the second and third sections differ: the mock server
also produced the `errors` field, the real server did not. Both conform to the contract,
because `errors` is not among the required fields.

The risk here is one-directional. Because the mock server also produces **optional
fields**, the client can assume they will always be present. That assumption collapses in
the move to the real server. There is no risk in the reverse direction: the required
fields the mock server produces are also present in the real server.

This is why whether the example generator produces optional fields should be a
configurable option. A client run in both modes has shown that it can stay standing with
only the required fields present — which is what the contract actually says.

## Summary

- The mock server turns the contract into something executable and lets the consumer
  develop without waiting for the provider.
- An example cannot be produced for fields that have a pattern; the schema must be
  complete enough that an example can be produced from it, and the gap shows up while the
  server is starting.
- The same client code runs unchanged on the mock and the real server; the success body's
  field set is the same on both, only the values differ.
- A mock server that only produces a success example runs one of the client's four
  branches and leaves the error paths never written.
- Being able to choose the status code requested from the mock server brings the count of
  branches run up to the same level as the real server.
- Because the mock server also produces optional fields, it can convince the client they
  will always be present; the generator should also have a mode that works with only the
  required fields.

## Next Step

Throughout this topic, the contract was always built the same way: the server decides
which resource to give with which fields, and the client makes do with what it is given.
Partial response and field selection loosened this rigidity, but the server still drew the
boundary. The query-based approach reverses this relationship: a type system is defined on
the server, the **client** decides which fields come back, and the response takes the
shape of the query. The next topic builds out this approach in detail; its first lesson
writes the schema and type system, defines object, scalar, interface, and union types, and
demonstrates them on a small runtime that actually resolves types.
