Generating Mock APIs from OpenAPI with Prism
Frontend teams blocked on an unbuilt backend hit the same wall: the API contract is agreed, the OpenAPI spec is written, but the provider implementation does not yet exist. Every day of waiting is either idle time or divergent hand-rolled stubs that will mismatch the real API at integration. This guide is part of Mock Server Strategies and shows exactly how to stand up a contract-accurate mock server from openapi.yaml using Prism 5 — covering static example serving, dynamic schema generation, the validation proxy mode, and a Dockerized setup for CI.
Symptom
The consumer team is ready to develop and test, but hitting the real API is blocked because:
- The backend provider is not yet implemented, only the
openapi.yamlcontract exists. - A hand-rolled stub is returning payloads that do not match the spec, causing deserialization errors like
TypeError: Cannot read properties of undefined (reading 'status')in the consumer integration suite. - CI fails because the consumer’s integration tests target a staging URL that is unavailable in the pipeline.
The immediate failure may also appear as Prism itself refusing to start if the spec has structural problems:
[1:10:23 AM] › [CLI] … ERROR Spec loading failed. [...]
[ValidationError]: Required property 'responses' is missing for operation GET /orders/{id}
Root Cause
Hand-rolled stubs break because they are maintained separately from the contract and drift silently. Unavailable staging environments fail CI because there is no way to gate on an external service reliably. Both problems share the same root: the mock is not derived from the spec.
Prism 5 solves this by reading openapi.yaml directly and synthesizing HTTP responses from the schema and named examples declared there. Because the spec is the single source of truth and Prism reloads it on start, the mock cannot return a shape the schema forbids. When you need to validate that the consumer sends correct requests too, Prism’s proxy mode enforces the spec on both directions of traffic — before the real provider exists.
Step-by-Step Fix
1. Install Prism 5 and Verify the Spec
Pin the major version so CI and local environments match exactly.
# Install Prism 5 CLI as a dev dependency
npm install --save-dev @stoplight/prism-cli@5
# Confirm the version
npx prism --version
# => 5.x.x
# Validate the spec before starting the mock — catches structural errors early
npx prism mock openapi.yaml --dry-run
Why this works: Prism 5 changed how it handles OpenAPI 3.1 type arrays (e.g. type: ["string", "null"]) and tightened request validation. Pinning to @5 prevents silent upgrades that change validation strictness. The --dry-run flag parses and validates the spec without binding a port, so you see schema errors before any consumer tries to connect.
2. Add Named Examples to openapi.yaml
Prism serves a named example verbatim when one exists for the matching operation and status code. Without examples it falls back to schema-synthesized values (dynamic mode). Named examples are preferable for consumer tests because they produce deterministic, assertable payloads.
# openapi.yaml — add named examples under each response's content block
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
paths:
/orders/{id}:
get:
operationId: getOrder
parameters:
- name: id
in: path
required: true
schema:
type: string
pattern: '^ord_[a-z0-9]+$'
responses:
'200':
description: Order found
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
examples:
# "shipped" is the default example; Prism returns this first
shipped:
value:
id: ord_123
status: SHIPPED
total: 4200
# A second named example for a different state
pending:
value:
id: ord_456
status: PENDING
total: 1500
'404':
description: Order not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
not_found:
value:
code: ORDER_NOT_FOUND
message: No order with that identifier exists
components:
schemas:
Order:
type: object
additionalProperties: false
required: [id, status, total]
properties:
id:
type: string
status:
type: string
enum: [PENDING, SHIPPED, CANCELLED]
total:
type: integer
description: Amount in minor currency units (e.g. cents)
Error:
type: object
required: [code, message]
properties:
code:
type: string
message:
type: string
Why this works: Prism resolves examples in the order they appear under examples. The first key wins unless you override with a Prefer header. Declaring additionalProperties: false on Order means Prism rejects any consumer request body that adds undeclared fields, enforcing the contract boundary automatically.
3. Start the Mock Server and Use the Prefer Header
# Default static mode: returns the first named example for the matched operation + status
npx prism mock openapi.yaml --port 4010
# Smoke test: returns the "shipped" example by default
curl -s http://localhost:4010/orders/ord_123
# => {"id":"ord_123","status":"SHIPPED","total":4200}
# Force the 404 response (and its named example) via Prefer header
curl -s -w '\nHTTP %{http_code}\n' \
http://localhost:4010/orders/ord_123 \
-H 'Prefer: code=404'
# => {"code":"ORDER_NOT_FOUND","message":"No order with that identifier exists"}
# => HTTP 404
# Request the "pending" named example explicitly
curl -s http://localhost:4010/orders/ord_456 \
-H 'Prefer: example=pending'
# => {"id":"ord_456","status":"PENDING","total":1500}
Why this works: The Prefer header is Prism’s mechanism for consumer tests to select specific contract states without changing the URL. Prefer: code=<status> routes to the matching response definition; Prefer: example=<name> selects a named example within that response. This makes it possible to test every documented contract state — success, 404, 422 — from a single running mock instance without restarts.
4. Enable Dynamic Mode for Schema-Generated Responses
When you need varied data — to fill UI states, exercise pagination, or feed a fuzzer — Prism can synthesize response bodies from the schema rather than returning a fixed example. Dynamic values are not deterministic, so do not use this mode for assertions in contract tests.
# Start in dynamic mode: Prism generates schema-conformant values on every request
npx prism mock openapi.yaml --port 4010 --dynamic
# Each call returns different valid data drawn from the schema
curl -s http://localhost:4010/orders/ord_abc
# => {"id":"Zd8f","status":"CANCELLED","total":9183}
curl -s http://localhost:4010/orders/ord_abc
# => {"id":"r7Lp","status":"PENDING","total":42}
Dynamic mode respects schema constraints: enum values are drawn from the declared list, pattern generates a matching string, minimum/maximum bound integers. The id field above will not match the ^ord_[a-z0-9]+$ pattern because Prism generates random strings — this is a known limitation when pattern and dynamic mode interact. For pattern-constrained fields, prefer named examples.
Why this works: --dynamic instructs Prism to use its faker engine instead of the example lookup. The engine traverses the JSON Schema and generates a structurally valid object. This is useful for smoke-testing UI rendering with varied data, not for asserting specific values.
Before/After Comparison
Before introducing Prism, the consumer team maintained a hand-rolled stub server:
// BEFORE: hand-rolled stub — diverges from the spec silently
app.get('/orders/:id', (req, res) => {
res.json({ orderId: req.params.id, state: 'shipped', amount: 42 });
// field names wrong: "orderId" not "id", "state" not "status", "amount" not "total"
});
The stub returns orderId, state, and amount — none of which match the OpenAPI schema field names id, status, and total. The consumer builds against this and ships code that breaks the moment it hits the real provider.
After pointing the consumer at Prism:
# AFTER: Prism serves the contract-accurate shape every time
curl -s http://localhost:4010/orders/ord_123
# => {"id":"ord_123","status":"SHIPPED","total":4200}
# ^^^ ^^^^^^ ^^^^^
# matches schema matches enum matches type: integer
The response field names, types, and enum values match the spec exactly. Any consumer code that passes against this mock will pass against any real provider that also satisfies the same contract — that is the guarantee Prism provides.
Validation Proxy Mode
In mock mode Prism validates the incoming request and returns the spec-derived response. In proxy mode it validates the request, forwards it to a real upstream, and validates the upstream’s response — catching contract violations in the provider before they reach production.
# Run the validation proxy: Prism sits between consumer and a real upstream
# (swap http://localhost:8080 with your staging URL or another local server)
npx prism proxy openapi.yaml http://localhost:8080 --port 4011
# A request with an invalid path parameter is rejected before reaching the upstream
curl -s -w '\nHTTP %{http_code}\n' \
http://localhost:4011/orders/INVALID-ID
# => HTTP 422
# => {
# "type": "https://stoplight.io/prism/errors#UNPROCESSABLE_ENTITY",
# "title": "Invalid request",
# "status": 422,
# "detail": "Your request/response is not valid",
# "validation": [
# {
# "location": ["path", "id"],
# "severity": "Error",
# "code": "pattern",
# "message": "must match pattern \"^ord_[a-z0-9]+$\""
# }
# ]
# }
# A conforming request passes through to the upstream and the response is validated
curl -s http://localhost:4011/orders/ord_123
# => upstream response, validated against the spec's 200 schema
Why this works: The proxy mode is the key tool for running Prism in front of a staging environment. Every request that reaches the upstream is guaranteed to conform to the spec; every response that returns to the consumer is validated against the declared response schema. This creates a two-sided contract enforcement point without modifying either the consumer or the provider.
Dockerizing Prism for CI
Running Prism in CI requires a reproducible, dependency-free artifact. A Docker image that bakes the spec in is the cleanest approach: the image version pins both Prism and the spec state, and it starts in under two seconds.
# Dockerfile.prism-mock
# syntax=docker/dockerfile:1
FROM stoplight/prism:5
# Copy the spec into the image at build time
# The spec is baked in, so the image version == the contract version
COPY openapi.yaml /usr/src/prism/openapi.yaml
# Expose on 4010 (Prism default)
EXPOSE 4010
# Start in static mock mode; add --dynamic if needed for a specific CI job
CMD ["mock", "--host", "0.0.0.0", "/usr/src/prism/openapi.yaml"]
Build and test the image locally:
docker build -f Dockerfile.prism-mock -t prism-orders-mock:local .
docker run --rm -d -p 4010:4010 --name prism-mock prism-orders-mock:local
# Wait for the server to be ready, then smoke-test
sleep 2
curl -s http://localhost:4010/orders/ord_123
# => {"id":"ord_123","status":"SHIPPED","total":4200}
docker stop prism-mock
Wire it into a GitHub Actions workflow as a service container so the mock is running before the consumer test job starts:
# .github/workflows/consumer-ci.yml
name: Consumer CI
on: [push, pull_request]
jobs:
consumer-tests:
runs-on: ubuntu-latest
services:
prism-mock:
image: ghcr.io/your-org/prism-orders-mock:${{ github.sha }}
ports:
- 4010:4010
# GitHub Actions health-checks the port before proceeding
options: >-
--health-cmd "wget -qO- http://localhost:4010/orders/ord_123 || exit 1"
--health-interval 5s
--health-timeout 3s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Run consumer integration tests
env:
API_BASE_URL: http://localhost:4010
run: npm test
Why this works: GitHub Actions service containers start before the job steps run and are accessible via localhost on the mapped port. The health-check blocks the job until Prism is responding, so there is no race between the mock starting and the test suite hitting it. Tagging the image with ${{ github.sha }} ties the spec version to the code revision, so a consumer branch always runs against the exact contract state it was written against.
Verification
After starting the mock, run these checks to confirm it is serving contract-accurate responses:
# 1. Confirm the default example is returned
curl -s http://localhost:4010/orders/ord_123 | \
python3 -c "import sys,json; d=json.load(sys.stdin); \
assert d['id']=='ord_123', 'id mismatch'; \
assert d['status'] in ('PENDING','SHIPPED','CANCELLED'), 'invalid status'; \
assert isinstance(d['total'], int), 'total must be integer'; \
print('OK: shape matches contract')"
# => OK: shape matches contract
# 2. Confirm 404 is served for the error path
curl -s -o /dev/null -w '%{http_code}' \
http://localhost:4010/orders/ord_123 -H 'Prefer: code=404'
# => 404
# 3. Confirm an invalid path param is rejected (422)
curl -s -o /dev/null -w '%{http_code}' \
http://localhost:4010/orders/UPPERCASE-INVALID
# => 422
# 4. In CI, assert the consumer test suite exits 0 against the mock
API_BASE_URL=http://localhost:4010 npm test
# All tests pass => mock is contract-accurate and consumer code is valid
A 422 on the invalid path parameter confirms Prism is enforcing the pattern constraint. If you see a 200 instead, check that you did not start Prism with --errors=false or --ignore-unknown-formats, which suppress validation.
Edge Cases and Caveats
-
Pattern constraints and dynamic mode interact poorly. Prism’s faker engine generates random strings for
type: stringfields; it does not always satisfy apatternconstraint. If your spec has tightly constrained identifiers (UUIDs, prefixed IDs), declare named examples to get deterministic, pattern-conformant values rather than relying on--dynamic. -
$refacross remote URLs requires network access at startup. Prism resolves all$refentries when it loads the spec. If your spec pulls schemas from a remote registry or a raw GitHub URL, CI runners with restricted egress will fail to start the mock. Bundle all referenced schemas into a single self-contained file usingnpx @redocly/cli bundle openapi.yaml -o openapi.bundle.yamlbefore passing it to Prism. -
The
Preferheader is consumed by Prism, not forwarded upstream. In proxy mode, Prism strips thePrefer: code=<n>andPrefer: example=<name>headers before forwarding the request. If your upstream has its ownPrefersemantics, use a different header name or run separate Prism instances for mock and proxy modes.
Frequently Asked Questions
Why does Prism return fabricated data instead of my named examples?
Prism uses dynamic mode when you pass the --dynamic flag or set the PRISM_DYNAMIC=true environment variable. Without those, it defaults to static mode and returns the first named example. Drop the flag or unset the variable, and Prism will return your example verbatim.
What is the difference between prism mock and prism proxy?
prism mock serves responses directly from the spec — no upstream needed. prism proxy forwards conforming requests to a real upstream and validates both the request and the upstream response against the spec, so you can run Prism in front of a staging server to catch contract violations in live traffic.
How do I make Prism return a 404 or error response in tests?
Send the Prefer: code=404 request header. Prism routes the request to the matching response definition for that status code and returns the example or schema-synthesized body declared there. You can combine it with Prefer: example=<name> to select a specific named example within that status.
Can Prism 5 validate response bodies, not just requests?
Yes, in proxy mode only. When running as a proxy Prism validates both the incoming request against the spec and the upstream response against the declared response schema. In mock mode it guarantees its own output conforms to the schema, but there is no separate response-validation hook.
Is Prism suitable for load testing?
Prism is single-threaded and not designed for high-throughput load testing. For load scenarios, generate a static fixture file from Prism’s output, serve it with a fast static server, or use a dedicated load-testing harness. Prism’s value is contract fidelity, not concurrency.
How do I handle OpenAPI $ref in Prism’s spec file?
Prism resolves $ref internally when it loads the spec, so you can use $ref across files as long as they are on the local filesystem or reachable HTTP URLs. Pass the root spec file path to prism mock; Prism follows all references from there automatically.