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
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| A mock built from the description | You want the mock in your own test process with no extra service | Code to maintain, and only the behavior you thought to write | A hosted mock already covers what you need |
| Hand-written JSON fixtures | A handful of cases, edited freely without touching the description | Three sources of truth, which drift within a release or two | The description already carries the payloads |
| Mockoon route responses | You want rules, delays and a desktop tool a tester can drive | A second definition of the API, kept in the tool rather than in the repository | The description is the artifact your team already reviews |
| Prism static mode | Any team that already publishes a description and wants a mock for free | A service to run, and behavior fixed by what the description says | You 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.
Related how-tos
Last verified
Verified 2026-09-14 against Node 22.22.2. Both output blocks are what the preceding command printed.
Footnotes
-
RFC 7240 is by J. Snell, dated June 2014, and it introduces
Preferas 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 makesPreference-Appliedworth echoing. The IANA registry the RFC set up holds thirteen preferences, six of them prefixedodata., andexampleis not among them. Prism’s convention is a convention, and the registry is the place it is not. ↩︎ Back to text -
The mock’s refusal carries
titleanddetail, 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 notypemember is taken to be of typeabout:blank, the one problem type the RFC registers itself, with a recommended status code given as not applicable. ↩︎ Back to text -
OAS 3.1.0 says of the Media Type Object that
exampleis mutually exclusive ofexamples. One row down it says thatexamplesis mutually exclusive ofexample, in case the first sentence had left a doubt. The Schema Object has anexampleof its own, marked deprecated in favor of theexampleskeyword 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