How-to › Test and mock integrations

How to serve your OpenAPI examples as mock responses#

Wire the examples in your OpenAPI description to what the mock server returns, so the docs, the tests and the mock all show one payload.

Audience
API producer
Level
beginner
Topic
Mock an API
Verified

A front-end team builds against a fixture folder, the docs site renders an example from the description, and the mock server returns something a third person wrote. All three show a different payload for the same endpoint. The bug report says the field is a string, and each of the three sources supports a different answer.

What you get

You will end up with a mock that answers from the description, so the payload a client sees in the docs is the payload it gets from the mock. You also get a check that every example still matches its schema. This is for you if you publish an API description and a mock.

Short answer

Put the payload in the description under examples, give each one a name, and let the mock serve it. A client picks a named example with a Prefer: example=<name> request header, which is the convention Prism established. Validate every example against its own schema in CI, because an example that drifts from the schema still renders in the docs and still ships.

You will need

An OpenAPI 3 description with at least one response example, and Node 22 or later. The example and examples keywords are defined in the Media Type Object, and the request header comes from RFC 7240.1

Approaches compared

ApproachWhen it fitsWhat it costs youWhen to pick something else
A mock built from the descriptionYou want the mock in your own test process with no extra serviceCode to maintain, and only the behavior you thought to writeA hosted mock already covers what you need
Hand-written JSON fixturesA handful of cases, edited freely without touching the descriptionThree sources of truth, which drift within a release or twoThe description already carries the payloads
Mockoon route responsesYou want rules, delays and a desktop tool a tester can driveA second definition of the API, kept in the tool rather than in the repositoryThe description is the artifact your team already reviews
Prism static modeAny team that already publishes a description and wants a mock for freeA service to run, and behavior fixed by what the description saysYou need stateful behavior across requests

The choice is really about how many places a payload lives. A description that carries its examples has one, and the mock, the docs and the contract tests all read it. Fixtures give you freedom to write a case the description does not describe, which is useful right up to the moment the two disagree and nobody notices. A hosted mock sits in between: one source, one more service to run.

Name every example

Named examples cost nothing at the point of writing and buy a great deal at the point of testing.

"examples": {
  "installed": {
    "summary": "A meter in service",
    "value": { "id": "mtr_8f2", "serial": "SN-40199", "state": "installed" }
  },
  "retired": {
    "summary": "A meter taken out of service",
    "value": { "id": "mtr_31a", "serial": "SN-40200", "state": "retired" }
  },
  "stale": {
    "summary": "Written before state became required",
    "value": { "id": "mtr_0b4", "serial": "SN-40201" }
  }
}

The names become the vocabulary a test uses. A test that asks for retired reads as a sentence, and the payload it gets is the one the docs render beside the endpoint. The summary field is what a docs renderer shows in the example picker, so write it for a reader rather than for yourself.

Three examples is usually the right number for an endpoint: the ordinary case, the interesting case, and the empty one. More than that and the picker in the docs becomes a menu nobody reads. Fewer, and the reader has to guess what an absent optional field means.

Let the header pick the example

One header does the selection, and an unknown name is an error rather than a fallback.2

const wanted = /example=([\w-]+)/.exec(req.headers.prefer ?? '')?.[1]
const names = Object.keys(media.examples)
const name = wanted ?? names[0]
if (!media.examples[name]) {
  return send(res, 400, { title: `No example named ${name}`, detail: `have: ${names.join(', ')}` })
}
res.setHeader('preference-applied', `example=${name}`)

Refusing an unknown name matters more than it looks. A mock that falls back to the first example turns a typo in a test into a passing test asserting the wrong payload. The failure then shows up much later, against the real API, in a pull request that changed nothing related.

Echo Preference-Applied so a caller can tell which example it actually received. Without it, a test cannot separate an honored request from a request the mock ignored while happening to return the right thing.

Check it worked

Validate the examples, then ask the mock for each of them.

node demo.mjs
-- every example checked against the schema it claims
installed  ok
retired    ok
stale      $.state: required property is missing
-- the mock answers with the example you asked for
example=installed  200 {"id":"mtr_8f2","serial":"SN-40199","state":"installed"}
example=retired    200 {"id":"mtr_31a","serial":"SN-40200","state":"retired"}
example=nope       400 {"title":"No example named nope","detail":"have: installed, retired, stale"}

The third line is the one that earns the check. That example was written before state became required, it still renders in the docs, and a client generated from those docs would expect a field the API always sends. Nothing about reading the description reveals it. Only running the example through its own schema does. Put that check in the same job that lints the description, so it runs on every change rather than on the days somebody remembers.

node --test mock.test.mjs
1..6
# tests 6
# suites 0
# pass 6
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 298.44952

When it goes wrong

Every paged request comes back as a 404 against a path that is clearly documented. The router is matching req.url, which carries the query string, against an OpenAPI path, which never does. Parse the URL and match only its path. It is the first thing a client hits and the last thing anyone suspects, because the path in the error message looks right.

The mock returns a generated payload rather than your example. The example sits on the schema instead of on the media type, or it uses example where the tool reads examples. Both keywords exist and they are not interchangeable.3

Every request gets the same body. The selection header is being dropped by a proxy, or the mock reads a query parameter you did not send. Assert on Preference-Applied rather than on the body alone.

An example renders correctly and fails validation. It is stale. Run the validation in CI, and fail the build rather than opening an issue nobody will pick up.

A test passes against the mock and fails against the real API. The example describes a payload the service never produces. Generate examples from a recorded response once, then keep them under the same review as the schema. A recorded payload with the identifiers replaced is worth more than an invented one, because it carries the fields nobody remembered to document.

When not to do this

Do not mock a workflow that depends on state. A description holds payloads, not transitions, so a create followed by a read returns the example rather than what you created. Use a real service for those paths, or a mock built to hold state.

Do not let the examples become the test data for everything. An example exists to show a reader what a response looks like, and one stretched to cover every edge case stops doing that job. Keep the examples readable, and put the awkward cases in tests where they can carry a comment.

Do not skip validating them because the schema is right. The schema and the example drift in opposite directions during a rename, and the example is the half nobody regenerates.

Last verified

Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.

Footnotes

  1. RFC 7240 is by J. Snell, dated June 2014, and it introduces Prefer as a header the server is allowed to ignore. A server that does not recognize a preference must ignore the token and carry on rather than fail, which is what makes Preference-Applied worth echoing. The IANA registry the RFC set up holds thirteen preferences, six of them prefixed odata., and example is not among them. Prism’s convention is a convention, and the registry is the place it is not. ↩︎ Back to text

  2. The mock’s refusal carries title and detail, two of the five members RFC 9457 defines for a problem details body. That document is dated July 2023 and obsoletes RFC 7807, which had said much the same in March 2016 under two of the same three names. A body with no type member is taken to be of type about:blank, the one problem type the RFC registers itself, with a recommended status code given as not applicable. ↩︎ Back to text

  3. OAS 3.1.0 says of the Media Type Object that example is mutually exclusive of examples. One row down it says that examples is mutually exclusive of example, in case the first sentence had left a doubt. The Schema Object has an example of its own, marked deprecated in favor of the examples keyword of JSON Schema. Section 9.5 of that specification defines it as an array, so its entries have no names. A payload can be written three ways under two spellings, and only one of the three lets a header pick it. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Get the Voxgig dispatch

Short notes on building SDKs, CLIs, REPLs, and MCPs for API-first teams, plus the occasional Fireside episode pick.

By signing up you agree to our Terms and Conditions.