Go from a single YAML file to a running mock server that answers like your real API — including the ugly moments when things go wrong.
Imagine your team is building a mobile app and a backend at the same time. The app needs data to show on screen, but the backend does not exist yet. So the app developer waits, or hard-codes fake JSON in ten different places and later has to rip it all out. Both options waste time.
There is a better way. If you write down what your API will look like in an OpenAPI spec, a small tool can read that file and pretend to be the server. Your app talks to the pretend server today, and switches to the real one later by changing a single URL.
By the end of this tutorial you will have:
- A small OpenAPI file that describes a “Tasks” API.
- A mock server running on your computer at
localhost:4010. - Realistic fake data that changes on every request.
- Error responses (404 and 422) that you can trigger on demand.
No backend code is needed. You should be comfortable typing a few commands in a terminal, and that is all. It takes about 10 minutes.
What is a mock API?
A mock API is a fake server. It listens for requests just like the real one, but instead of talking to a database it sends back pre-made answers. Think of a film set: the buildings look real from the camera’s side, but there is nothing behind the front wall. That is perfectly fine when all you need is the picture.
Here is the whole idea in one picture. You write the spec, the mock server reads it, and your app talks to the mock server.
The tool we will use is Prism, a free, open-source mock server from Stoplight. You do not install it permanently — npx downloads and runs it on demand.
What is an OpenAPI spec?
An OpenAPI spec is a text file (usually YAML) that describes an API in a way both humans and tools can read. It answers three questions: Which URLs exist? What can I send to them? What comes back? It works like a restaurant menu: it lists every dish, but it does not contain the kitchen.
The picture below shows a tiny piece of a spec, with each part labelled. You will see all four of these ideas again in Step 1.
| Word | Plain-English meaning | Example |
|---|---|---|
| Path | The address of something | /tasks |
| Method | What you want to do with it | GET = read, POST = create |
| Status code | A number that says how it went | 200 OK, 404 not found |
| Schema | The shape of the data | a task has a title (text) and a priority (1–5) |
| JSON | The text format the data travels in | {"title": "Buy milk"} |
Before you start
You need two things on your computer: Node.js (version 18.16 or newer — the current LTS release is a safe choice) and a terminal. Node comes with npx, the tool that will run Prism for us. Check that Node is installed:
node --versionIf you see a number like v20.11.0, you are ready. If the command is not found, install the current LTS version from nodejs.org first. Now make a folder for the project:
mkdir tasks-mock
cd tasks-mockWrite the spec
Create a file called openapi.yaml inside the folder and paste in the spec below. It describes three things: list tasks, create a task and fetch one task. It is long only because it is complete — most of it repeats a simple pattern.
openapi: 3.0.3
info:
title: Tasks API
version: 1.0.0
servers:
- url: http://localhost:4010
paths:
/tasks:
get:
summary: List tasks
parameters:
- name: limit
in: query
schema: { type: integer, minimum: 1, maximum: 50, default: 10 }
responses:
'200':
description: A list of tasks
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Task' }
example:
- id: 6f1c1d0e-5b7a-4f57-9d0e-2a1f3f0b9a11
title: Write the launch email
status: doing
priority: 2
post:
summary: Create a task
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/NewTask' }
responses:
'201':
description: The task was created
content:
application/json:
schema: { $ref: '#/components/schemas/Task' }
example:
id: 0b5f0a3e-7c2d-4c39-8f0f-6d6c1a2b3c44
title: Book the venue
status: todo
priority: 3
/tasks/{taskId}:
get:
summary: Get one task
parameters:
- name: taskId
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200':
description: The task
content:
application/json:
schema: { $ref: '#/components/schemas/Task' }
example:
id: 6f1c1d0e-5b7a-4f57-9d0e-2a1f3f0b9a11
title: Write the launch email
status: doing
priority: 2
'404':
description: No task with that id
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
example:
code: not_found
message: No task with that id
components:
schemas:
NewTask:
type: object
required: [title]
properties:
title: { type: string, minLength: 1 }
priority: { type: integer, minimum: 1, maximum: 5 }
Task:
type: object
required: [id, title, status]
properties:
id: { type: string, format: uuid }
title: { type: string }
status: { type: string, enum: [todo, doing, done] }
priority: { type: integer, minimum: 1, maximum: 5 }
Error:
type: object
required: [code, message]
properties:
code: { type: string }
message: { type: string }Do not worry about memorising it. Here is how to read it, top to bottom:
infoandservers— the name of the API and where it will live. Prism ignores the URL here, but tools that read the spec use it.paths— the list of URLs. We have/tasksand/tasks/{taskId}. The curly braces mean “any value goes here”, like a task id.getandpost— the methods each URL supports.GET /taskslists tasks;POST /taskscreates one.parameters— extra inputs.limitis an optional number in the URL (?limit=5) that must be between 1 and 50.responses— every answer the API can give, keyed by status code.'200'is success.'404'is “not found”. We write the codes in quotes because YAML would otherwise treat them as numbers.example— the exact JSON we want the mock to return. This is what makes the mock feel real.components → schemas— reusable shapes.Taskis defined once and reused with$refso we never repeat ourselves.
Start the mock server
In the same folder, run this single command:
npx @stoplight/prism-cli mock openapi.yamlThe first time, npx asks to download the package — say yes. After a few seconds you will see something like this (the exact text differs a little between versions):
[CLI] … awaiting Starting Prism…
[HTTP SERVER] get /tasks ✔ success Prism is listening on http://127.0.0.1:4010
[HTTP SERVER] post /tasks
[HTTP SERVER] get /tasks/{taskId}Prism listed every route it found in your spec, which is a good sign that the file is valid. Leave this terminal window open — the server runs for as long as the command runs. Press Ctrl + C to stop it.
What happens when a request reaches Prism? It follows the same five steps every time. Knowing them will make the rest of the tutorial easy to understand.
Make your first request
Open a second terminal window (keep Prism running in the first one) and ask for the list of tasks:
curl -i http://127.0.0.1:4010/tasksThe -i flag also prints the response headers, which is handy for seeing the status code. You should get back the example you wrote in the spec:
HTTP/1.1 200 OK
content-type: application/json
[{"id":"6f1c1d0e-5b7a-4f57-9d0e-2a1f3f0b9a11","title":"Write the launch email","status":"doing","priority":2}]That is your first mock response. Try fetching a single task, and creating a new one:
curl -i http://127.0.0.1:4010/tasks/6f1c1d0e-5b7a-4f57-9d0e-2a1f3f0b9a11
curl -i -X POST http://127.0.0.1:4010/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Book the venue", "priority": 3}'The POST returns 201 Created with the example task from your spec. Notice that it always says “Book the venue”, even if you sent a different title. This is normal: a mock does not store anything. It only checks that your request is valid and then replies with the prepared answer.
Get realistic data
Prism can choose a response in three different ways. So far we have used the first one.
To switch on dynamic mode, stop Prism with Ctrl + C and start it again with the -d flag:
npx @stoplight/prism-cli mock -d openapi.yamlNow call GET /tasks a few times. Instead of your example, Prism reads the schema and generates data that fits it:
curl -s http://127.0.0.1:4010/tasks
# first call (yours will differ)
[{"id":"a3f0c2d4-…","title":"quis nostrud","status":"done","priority":4}]
# second call
[{"id":"91be77aa-…","title":"lorem ipsum","status":"todo","priority":1}]Look at how each rule in the schema shaped the data: format: uuid made a real-looking id, enum picked one of the three allowed statuses, and minimum/maximum kept the priority between 1 and 5. The richer your schema, the more believable the fake data. Notice too that dynamic mode ignores your example values completely and works only from the schema.
You can also ask for dynamic data on a single request, without restarting Prism, by adding a header: Prefer: dynamic=true.
Try the failure modes
Real APIs fail. Servers say “not found”, and users send bad data. A good mock lets you see those moments before launch, so your app never shows a blank screen or a spinner that never stops.
Force a specific response with Prefer
Add a Prefer header to ask for a status code that is documented in your spec. We documented a 404 for “get one task”, so:
curl -i -H "Prefer: code=404" \
http://127.0.0.1:4010/tasks/6f1c1d0e-5b7a-4f57-9d0e-2a1f3f0b9a11HTTP/1.1 404 Not Found
content-type: application/json
{"code":"not_found","message":"No task with that id"}Your frontend developer can now build and test the “task not found” screen without touching a backend.
Send a bad request to get a 422
Prism also checks what you send. Try creating a task without the required title:
curl -i -X POST http://127.0.0.1:4010/tasks \
-H "Content-Type: application/json" \
-d '{"priority": 3}'Prism answers with 422 Unprocessable Entity and explains what is wrong, shortened here:
HTTP/1.1 422 Unprocessable Entity
{
"title": "Invalid request",
"status": 422,
"validation": [{
"location": ["body"],
"message": "Request body must have required property 'title'"
}]
}The same thing happens if you break a rule in the URL. Try ?limit=100 on GET /tasks: our spec says the maximum is 50, so Prism rejects it. This is a quiet superpower — your frontend’s mistakes are caught early, because the mock enforces the contract.
| What you send | What you get | Why |
|---|---|---|
GET /tasks | 200 with an example | Valid request, default mode |
Prefer: code=404 on GET /tasks/{id} | 404 with the error example | You asked for a documented response |
POST /tasks with no title | 422 with details | Body breaks the schema |
GET /tasks?limit=100 | 422 with details | limit is above the maximum of 50 |
GET /nope | 404 route error | That path is not in the spec |
Point your app at the mock
The last step is to use it. The trick is to keep the API address in one place — a setting — never spread through your code. Then switching between the mock and the real server is a one-line change.
// api.js
const API_URL = process.env.API_URL ?? "http://localhost:4010";
export async function listTasks(limit = 10) {
const res = await fetch(`${API_URL}/tasks?limit=${limit}`);
if (!res.ok) throw new Error(`Request failed: ${res.status}`);
return res.json();
}Run your app with API_URL unset while you develop, and set it to your real server when the backend is ready:
# develop against the mock
npm run dev
# later, use the real API
API_URL=https://api.example.com npm run devTwo practical notes:
- Browsers and CORS. A web page on
localhost:3000callinglocalhost:4010is a cross-origin request. Prism sends CORS headers by default, so this works without extra setup. - Docker or a teammate’s laptop. Prism listens on
127.0.0.1(only your machine) by default. Add-h 0.0.0.0to let other machines reach it:npx @stoplight/prism-cli mock -h 0.0.0.0 openapi.yaml.
When something goes wrong
| What you see | Most likely cause | Fix |
|---|---|---|
command not found: npx | Node.js is not installed | Install the current Node.js LTS and reopen the terminal |
| Prism exits with a parsing error | A YAML indentation mistake | Use spaces (never tabs) and check the line number in the message |
address already in use | Something else is on port 4010 | Pick another port: -p 4011 |
You always get 404 | The path or method is not in the spec | Compare your URL with the paths section, including the method |
A 422 you did not expect | Your request breaks the schema | Read the validation part of the response — it names the field |
Prefer header seems ignored | That status code is not in the spec | Add the response to the spec, then restart Prism |
| You edited the spec but nothing changed | Prism loaded the old file | Stop with Ctrl + C and start it again |
Where to go next
You now have a working mock, but the bigger win is the habit behind it: design the API on paper first. A few ideas to build on this:
- Commit
openapi.yamlto your repository next to the code, so the spec changes in the same pull request as the API. - Add a
package.jsonscript such as"mock": "prism mock -d openapi.yaml"so anyone on the team can start it withnpm run mock. - Write frontend tests against the mock using the default example mode, so results are repeatable.
- Later, check the real API against the same spec to catch cases where the two drift apart.
If you would rather not run anything locally, JarAPI is our tool for mocking, testing and monitoring an API from an OpenAPI spec. And once your app is running against the mock, our next tutorial shows how to give every pull request its own live URL: Ship Preview Environments for Every Pull Request.