Creating an OpenAPI Mock Server with Prism

Prism is a free, open source tool created by Stoplight that enables developers to quickly create mock API servers using only an OpenAPI document. Prism parses your OpenAPI spec to understand your endpoints, schemas, and examples to spin up a local server you can make API calls to using whatever client you want.

This is invaluable because it allows developers to test the API experience before they write any real code. With a mock server, developers can spot potential issues early in the API design process and quickly iterate by modifying a YAML or JSON document instead of having to make expensive code changes.

Here's how to get Prism set up on your machine.

First, we install Prism using npm or yarn.

npm install -g @stoplight/prism-cli

# OR

yarn global add @stoplight/prism-cli

Next, we'll need an OpenAPI document. Use one you have already, get a copy of the standard sample OpenAPI document of a Petstore API, or use the one I created in Creating an OpenAPI Specification the Hard Way.

Now it's as easy as running Prism and referencing your OpenAPI file.

prism mock my_open_api_doc.yaml

If all goes well, your output should be a list of all your endpoints with local server paths.

[2:59:43 PM] › [CLI] …  awaiting  Starting Prism…
[2:59:43 PM] › [CLI] ℹ  info      GET        http://127.0.0.1:4010/definitions/tortellini
[2:59:43 PM] › [CLI] ℹ  info      PUT        http://127.0.0.1:4010/definitions/tortellini
[2:59:43 PM] › [CLI] ℹ  info      DELETE     http://127.0.0.1:4010/definitions/tortellini
[2:59:43 PM] › [CLI] ℹ  info      POST       http://127.0.0.1:4010/definitions
[2:59:43 PM] › [CLI] ▶  start     Prism is listening on http://127.0.0.1:4010

If the command fails, it's likely due to minor syntax issues in your document. Use a YAML or JSON validator to check for those. Or check out one of these OpenAPI linters.

Assuming everything is working, let's do a simple GET to our local server at http://127.0.0.1:4010.

curl -i http://127.0.0.1:4010/definitions/tagliatelle

Ideally, we get a response like:

curl -i http://127.0.0.1:4010/definitions/tagliatelle
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Content-type: application/json
Content-Length: 225
Date: Mon, 29 Jan 2024 14:18:59 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":40125,"word":"tagliatelle","created_at":"2023-12-06T04:55:36.887Z","updated_at":"2022-12-06T04:55:36.887Z","meanings":[{"id":333,"part_of_speech":"noun","meaning":"A type of pasta shaped into long, thin, flat strips"}]}

The response body is constructed by whatever we put in the responses child object of any API path. In this case, our GET endpoint references the definitions schema object which includes the above sample data.

Your mock server will also give you server logs notifying you of any validation issues. Here's what a good GET event should look like:

[3:18:59 PM] › [HTTP SERVER] get /definitions/tortellini ℹ  info      Request received
[3:18:59 PM] ›     [NEGOTIATOR] ℹ  info      Request contains an accept header: */*
[3:18:59 PM] ›     [VALIDATOR] ✔  success   The request passed the validation rules. Looking for the best response
[3:18:59 PM] ›     [NEGOTIATOR] ✔  success   Found a compatible content for */*
[3:18:59 PM] ›     [NEGOTIATOR] ✔  success   Responding with the requested status code 200

And if we try a route like /v2/definitions/tagliatelle that doesn't exist in our OpenAPI spec, we see Prism handles that as expected with a 404 Not Found response.

curl -i http://127.0.0.1:4010/v2/definitions/tagliatelle
HTTP/1.1 404 Not Found
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
content-type: application/problem+json
Content-Length: 218
Date: Tue, 30 Jan 2024 09:01:27 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"type":"https://stoplight.io/prism/errors#NO_PATH_MATCHED_ERROR","title":"Route not resolved, no path matched","status":404,"detail":"The route /v2/definitions/tagliatelle hasn't been found in the specification file"}

We can also test a POST request protected with Basic Auth. If you've protected your API with Basic Auth, Prism will expect to see a Basic Auth header included in the request. Of course it can't actually validate the token, but it's helpful enough in the early development phase of an API

curl -i --request POST \
  --url http://127.0.0.1:4010/definitions \
  --header 'Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=' \
  --header 'Content-Type: application/json' \
  --data '{
  "word": "tagliatelle",
  "meanings": [
    {
      "part_of_speech": "noun",
      "meaning": "A type of pasta shaped into long, thin, flat strips"
    }
  ]
}'
HTTP/1.1 201 Created
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Content-type: application/json
Content-Length: 225
Date: Mon, 29 Jan 2024 14:36:02 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":40125,"word":"tagliatelle","created_at":"2022-12-06T04:55:36.887Z","updated_at":"2022-12-06T04:55:36.887Z","meanings":[{"id":333,"part_of_speech":"noun","meaning":"A type of pasta shaped into long, thin, flat strips"}]}

Prism will also handle the various erroneous requests we might want to test. You can try removing the Basic Auth header, and you'll get a 401 Unauthorized.

curl -i --request POST \
  --url http://127.0.0.1:4010/definitions \
  --header 'Content-Type: application/json' \
  --data '{
  "word": "tagliatelle",
  "meanings": [
    {
      "part_of_speech": "noun",
      "meaning": "A type of pasta shaped into long, thin, flat strips"
    }
  ]
}'
HTTP/1.1 401 Unauthorized
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
sl-violations: [{"location":["request"],"severity":"Error","code":401,"message":"Invalid security scheme used"}]
Date: Mon, 29 Jan 2024 14:40:17 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 0

Changing the Content-Type header to application/xml when our API doesn't support XML returns a 422 Unprocessable Entity.

curl -i --request POST \
  --url http://127.0.0.1:4010/definitions \
  --header 'Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=' \
  --header 'Content-Type: application/xml' \
  --data '{
  "word": "tagliatelle",
  "meanings": [
    {
      "part_of_speech": "noun",
      "meaning": "A type of pasta shaped into long, thin, flat strips"
    }
  ]
}'
HTTP/1.1 422 Unprocessable Entity
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
sl-violations: [{"location":["request"],"severity":"Error","code":415,"message":"Supported content types: application/json"}]
Date: Mon, 29 Jan 2024 14:41:51 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Content-Length: 0

Adding a pronunciation field to our request body that doesn't exist in our schema returns the expected 400 Bad Request error.

curl -i --request POST \
  --url http://127.0.0.1:4010/definitions \
  --header 'Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=' \
  --header 'Content-Type: application/json' \
  --data '{
  "pronunciation: "taʎʎaˈtɛlle",
  "word": "tagliatelle",
  "meanings": [
    {
      "part_of_speech": "noun",
      "meaning": "A type of pasta shaped into long, thin, flat strips"
    }
  ]
}'
HTTP/1.1 400 Bad Request
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Content-Type: application/json; charset=utf-8
Content-Length: 58
Date: Mon, 29 Jan 2024 14:42:42 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"error":{"code":"invalid_json","message":"Invalid JSON"}}

A Prism mock server is no replacement for a fully working prototype, but it is an invaluable tool for the early design phase of an API as stakeholders are negotiating over how the API should function. It's certainly worth a whirl.

You've successfully subscribed to Alex Barron
Great! Next, complete checkout to get full access to all premium content.
Error! Could not sign up. invalid link.
Welcome back! You've successfully signed in.
Error! Could not sign in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.