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.

Prism response selection Prism validates the request, then returns the example named by the Prefer header, the first example, or a value generated from the schema. request from the client validate 422 if it violates Prefer names an example exactly what you wrote first example in the spec realistic if you wrote one generated from the schema "string", 0 — type-correct noise quality of the mock = quality of your examples

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
Stub drift versus generated mock When the contract changes, a hand-written stub keeps passing and hides the divergence, while a generated mock fails the client tests immediately. the contract changes — what happens next? hand-written stub returns the old shape client tests still pass divergence found at integration generated mock returns the new shape client tests fail today divergence found in CI

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.

Where to run the mock In the CI job for isolation, as a shared service for exploration, or in a container beside the application for integration suites. three placements, three trade-offs inside the CI job fresh per run, pinned version nothing shared default choice shared service good for exploring and for a portal console someone must keep it current container beside the app for integration suites needing several services more moving parts

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.