Generating Mock Servers from OpenAPI with Prism
This guide is part of Mock Servers & Contract Testing from OpenAPI within OpenAPI & AsyncAPI Schema Authoring. It covers standing up a working mock from a specification so consumer development can start before the API exists — and, just as usefully, so client test suites stop depending on a shared staging environment that somebody is always in the middle of redeploying.
Problem & Context
Consumer teams block on producer teams for a predictable reason: there is nothing to call. The API is designed, the specification exists, and the implementation is three sprints away — so the client work either stops, or proceeds against hand-written stubs that encode somebody’s guess about the response shape and then have to be rewritten when the real API arrives.
Hand-written stubs are worse than no stubs in one specific way: they cannot go stale visibly. When the contract changes, the stub keeps returning its old shape, the client tests keep passing, and the divergence is discovered at integration time — which is exactly the moment the mock was supposed to make cheap.
A mock generated from the specification cannot drift, because it is the specification. When the contract changes, the mock changes with it and the client’s tests fail immediately, which is the correct time to find out. What determines whether that mock is genuinely useful is entirely the quality of the examples in the document.
Step-by-Step Solution
1. Bundle the specification
Prism reads one document, so resolve any multi-file structure first:
npm install --save-dev @stoplight/[email protected] @redocly/cli@2
npx @redocly/cli@2 bundle openapi/api.yaml -o dist/api.bundled.yaml
2. Start the mock
npx prism mock dist/api.bundled.yaml --port 4010
[CLI] ℹ info GET http://127.0.0.1:4010/invoices
[CLI] ℹ info POST http://127.0.0.1:4010/invoices
[CLI] ℹ info GET http://127.0.0.1:4010/invoices/{id}
[CLI] ▶ start Prism is listening on http://127.0.0.1:4010
Every documented operation is served immediately, with no configuration file. If an operation you expected is missing, it is missing from the bundled document — check the bundle, not Prism.
3. Add response examples
This is the step that decides whether the mock is useful. Named examples give consumers realistic data and a way to select scenarios:
paths:
/invoices/{id}:
get:
operationId: getInvoice
responses:
'200':
description: The invoice
content:
application/json:
schema: { $ref: '#/components/schemas/Invoice' }
examples:
paid:
summary: A settled invoice
value:
id: inv_01H8XK
status: paid
total_cents: 24900
currency: EUR
issued_at: '2026-07-01T09:12:00Z'
overdue:
summary: Past its due date, with a late fee applied
value:
id: inv_01H8XM
status: overdue
total_cents: 26400
currency: EUR
issued_at: '2026-05-01T09:12:00Z'
'404':
description: No such invoice
content:
application/problem+json:
example:
type: https://acme.com/problems/not-found
title: Invoice not found
status: 404
4. Select branches with the Prefer header
Named examples and documented status codes both become addressable, which is what lets a client test its error handling:
curl -s http://127.0.0.1:4010/invoices/inv_1 -H 'Prefer: example=overdue' | jq -r .status
overdue
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4010/invoices/nope -H 'Prefer: code=404'
404
5. Wire the mock into consumer CI
Start it in the background, wait for it to accept connections, and point the client’s own suite at it:
- name: Start the mock
run: |
npx prism mock dist/api.bundled.yaml --port 4010 &
npx wait-on http://127.0.0.1:4010/invoices --timeout 30000
- name: Run consumer tests against the contract
run: npm test
env:
API_BASE_URL: http://127.0.0.1:4010
Complete Working Example
A script that bundles, asserts the examples exist, starts the mock, and exercises every branch — safe to run locally and in CI:
#!/usr/bin/env bash
# mock-check.sh — start a contract mock and prove it serves useful responses.
set -euo pipefail
SPEC="${SPEC:-openapi/api.yaml}"
PORT="${PORT:-4010}"
BUNDLE=dist/api.bundled.yaml
mkdir -p dist
npx @redocly/cli@2 lint "${SPEC}"
npx @redocly/cli@2 bundle "${SPEC}" -o "${BUNDLE}"
# A mock with no examples is type-correct noise. Refuse to start one.
examples=$(yq -r '[.paths[][]?.responses[]?.content[]?
| (.examples // {} | keys | length) + (if .example then 1 else 0 end)]
| add // 0' "${BUNDLE}")
if [ "${examples}" -eq 0 ]; then
echo "ERROR: the specification has no response examples — the mock would be useless" >&2
exit 1
fi
echo "OK ${examples} example(s) found"
npx prism mock "${BUNDLE}" --port "${PORT}" &
PRISM_PID=$!
trap 'kill "${PRISM_PID}" 2>/dev/null || true' EXIT
npx wait-on "http-get://127.0.0.1:${PORT}/invoices" --timeout 30000
BASE="http://127.0.0.1:${PORT}"
# Default response
code=$(curl -s -o /dev/null -w '%{http_code}' "${BASE}/invoices/inv_1")
[ "${code}" = "200" ] && echo "OK default response is 200"
# Named example selection
status=$(curl -s "${BASE}/invoices/inv_1" -H 'Prefer: example=overdue' | jq -r .status)
[ "${status}" = "overdue" ] && echo "OK Prefer: example=overdue selected the right branch"
# Documented error path
code=$(curl -s -o /dev/null -w '%{http_code}' "${BASE}/invoices/x" -H 'Prefer: code=404')
[ "${code}" = "404" ] && echo "OK Prefer: code=404 returned the documented error"
# Request validation — an invalid body must be rejected by the mock too
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "${BASE}/invoices" \
-H 'content-type: application/json' -d '{"total_cents":"not-a-number"}')
[ "${code}" = "422" ] && echo "OK invalid request rejected with 422"
Expected output:
OK 14 example(s) found
OK default response is 200
OK Prefer: example=overdue selected the right branch
OK Prefer: code=404 returned the documented error
OK invalid request rejected with 422
The example-count guard is the most valuable line in that script. A mock with no examples starts happily and serves nonsense, and the resulting “the mock is useless” conclusion usually gets blamed on the tool rather than on the specification.
Gotchas & Edge Cases
Request validation surprises client authors. Prism validates inbound requests against the contract and returns 422 for violations. That is correct behaviour and catches real client bugs, but a team expecting a permissive stub reads it as the mock being broken. Point them at the validation detail in the response body, which names the offending field.
Dynamic mode returns different data every call. --dynamic generates fresh values from the schema per request, which is excellent for finding clients that hardcode an id and useless for a snapshot test. Pick one mode per test suite deliberately rather than switching midway.
The mock has no state. A POST followed by a GET will not return the created resource, because there is nothing behind the mock. Tests that assume persistence need either a stateful mock or a real environment; do not spend effort making Prism pretend.
Path parameters are not validated for existence. Any {id} matches, so a test asserting that an unknown id returns 404 must ask for it with Prefer: code=404. Nothing about the mock knows which ids “exist”.
A stale bundle silently mocks an old contract. The bundle is a build artifact; if CI caches it or a developer forgets to rebuild, the mock serves last week’s API and the client tests pass against the wrong thing. Bundle as part of the same command that starts the mock, as the script above does, so the two cannot separate.
Where the mock should run is a design decision, not an afterthought. Three placements are common and they solve different problems. Running it as a background process inside the consumer’s CI job, as the workflow above does, is the cheapest and most isolated — every run gets a fresh mock at a known contract version and nothing is shared. Running it as a long-lived shared service on an internal host is convenient for manual exploration and for pointing a portal console at, but it reintroduces the shared-environment problem the mock was meant to remove: someone has to keep it updated, and a stale one is worse than none. Running it in a container alongside the application under test, wired in through a compose file, sits in between and works well for integration suites that need several services at once.
Version the mock the same way you version everything else. Whichever placement you choose, the mock should be started from a pinned contract version rather than from whatever is on a branch, for exactly the reasons covered in Versioning OpenAPI specs with Git tags. A consumer test suite that passes against an unreleased contract is not evidence of anything, and the failure it hides — a client built against endpoints that do not exist yet — is precisely the one the mock was supposed to prevent.
FAQ
Why does the mock return "string" and 0 for every field?
The operation has no examples, so Prism generated values from the schema. Add named examples to the response content; the mock prefers an example over generation and returns exactly what you wrote. This is a specification improvement that helps the rendered reference too.
Can the mock return an error response on demand?
Yes. Send Prefer: code=404 to request a specific documented status, or Prefer: example=name to select a named example. This is what makes a mock useful for testing error handling rather than only the happy path — and error handling is where most client bugs live.
Should the mock validate the requests my client sends?
Yes, and it does by default. A request that violates the contract gets a 422 with the validation detail, which catches client bugs the real API would also reject — just several weeks earlier.
Related
- Mock Servers & Contract Testing from OpenAPI — the parent guide and the producer-side half.
- Contract testing OpenAPI with Schemathesis — verifying the real implementation.
- Example Payload Management — keeping the examples this depends on accurate.
- Interactive API Consoles & Try-It Panels — pointing a portal console at this mock.