Making the Most of OpenAPI
We've been using OpenAPI for a while now, both for testing server-side APIs and for auto-generating frontend client code.
- Schema-first API development with committee, OpenAPI, and Rails
- A rich, type-safe client life with auto-generation using OpenAPI Generator + TypeScript
The Current Problem
Even with auto-generated client code in hand, it's still inconvenient to move forward with markup and integration work if there's no data to return. We've already generated types from the schema, so it feels wasteful to build things with placeholder data and then fix everything up later to match the real data.
There's also the common situation where, especially in the early stages of a project, the backend and frontend developers are each working in their own local environments. In that case, asking the frontend developer to "just run the API server locally" can be a high bar to clear. Sure, you could set up the programming language and framework locally, or use Docker, but having to do that kind of setup and verification every single time isn't ideal.
Since implementing the API tends to block frontend integration work no matter what, we think it would be ideal if the backend and frontend could start development at the same time, as soon as the OpenAPI schema is defined.
From OpenAPI to a Mock Server
One way to decouple backend and frontend implementation is to use a mock server.
As mentioned above, spinning up a local API server and preparing test data during the early markup and integration phase is a heavy burden, and preparing separate data just for a mock server isn't very efficient either. Ideally, we'd want something that works with just an OpenAPI definition file and a single command, with no complex setup required.
If we take "the OpenAPI schema is the source of truth" as a premise, the backend can implement to satisfy the schema, while the frontend can proceed with integration on the assumption that it will receive data based on that schema.
The tool that makes this possible is Stoplight's Prism.
- Stoplight: Prism
Note: Stoplight develops several tools for streamlining API development, and Stoplight Studio, which lets you edit OpenAPI schemas via a GUI, is also worth checking out.
With Prism, you can easily spin up a mock server that validates requests and returns dummy responses, all based on an OpenAPI schema.
Using Prism, once the OpenAPI schema is ready, the frontend team could simply spin up a mock server with Prism and start implementing right away.
Trying Out Prism
Installation
Prism has several installation methods, but for frontend engineers, npm or yarn is probably the easiest to follow.
# Install however you prefer
$ npm install -g @stoplight/prism-cli
# The version we tested with was 4.1.0
$ prism mock --version
4.1.0
Starting the Mock Server
Let's start by running Prism against a sample OpenAPI schema file included in the repository. When we did, a list of endpoints appeared and the mock server started up successfully. The port number and other settings can be changed via options.
$ prism mock https://raw.githubusercontent.com/stoplightio/prism/master/examples/petstore.oas3.yaml
[18:26:51] › [CLI] … awaiting Starting Prism…
[18:26:52] › [CLI] ℹ info GET http://127.0.0.1:4010/no_auth/pets?name=saepe
[18:26:52] › [CLI] ℹ info POST http://127.0.0.1:4010/no_auth/pets
[18:26:52] › [CLI] ℹ info GET http://127.0.0.1:4010/no_auth/pets/findByStatus?status=sold&status=available&status=pending&status=pending&status=sold&status=available&status=available&status=available&status=pending&status=pending&status=sold&status=sold&status=available&status=available&status=sold&status=pending&status=available&status=pending&status=pending&status=sold
[18:26:52] › [CLI] ℹ info GET http://127.0.0.1:4010/no_auth/pets/findByTags?tags=aut&tags=id&tags=ut&tags=sapiente&tags=molestiae&tags=nostrum&tags=magni&tags=facere&tags=enim&tags=vero&tags=sit&tags=beatae&tags=consequatur&tags=facere&tags=unde&tags=eveniet&tags=aut&tags=et&tags=rem&tags=sint
[18:26:52] › [CLI] ℹ info GET http://127.0.0.1:4010/no_auth/pets/421
[18:26:52] › [CLI] ℹ info POST http://127.0.0.1:4010/no_auth/pets/801
[18:26:52] › [CLI] ℹ info POST http://127.0.0.1:4010/pets
[18:26:52] › [CLI] ℹ info PUT http://127.0.0.1:4010/pets
[18:26:52] › [CLI] ℹ info GET http://127.0.0.1:4010/pets/findByStatus?status=available&status=available&status=available&status=sold&status=pending&status=pending&status=available&status=available&status=pending&status=available&status=available&status=sold&status=pending&status=sold&status=pending&status=available&status=available&status=available&status=sold&status=available
[18:26:52] › [CLI] ℹ info GET http://127.0.0.1:4010/pets/findByTags?tags=modi&tags=laudantium&tags=ea&tags=nihil&tags=ab&tags=quod&tags=quis&tags=voluptatem&tags=quia&tags=et&tags=cum&tags=et&tags=dolor&tags=odit&tags=deleniti&tags=est&tags=voluptate&tags=molestias&tags=illo&tags=id
[18:26:52] › [CLI] ℹ info GET http://127.0.0.1:4010/pets/785
[18:26:52] › [CLI] ℹ info POST http://127.0.0.1:4010/pets/863
[18:26:52] › [CLI] ℹ info DELETE http://127.0.0.1:4010/pets/973
[18:26:52] › [CLI] ℹ info POST http://127.0.0.1:4010/pets/939/uploadImage
# Some lines omitted
[18:26:52] › [CLI] ▶ start Prism is listening on http://127.0.0.1:4010
Let's hit a few endpoints and check the behavior.
GET http://127.0.0.1:4010/no_auth/pets?name=saepe
Accessing this endpoint returns the response defined in the schema's example.
$ curl -i 'http://127.0.0.1:4010/no_auth/pets?name=saepe'
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: 34
Date: Fri, 23 Oct 2020 09:39:42 GMT
Connection: keep-alive
Keep-Alive: timeout=5
[{"name":"a_name","photoUrls":[]}]
By the way, this endpoint requires the name query parameter, so if the parameter is missing, it returns a 422 validation error.
$ curl -i 'http://127.0.0.1:4010/no_auth/pets'
HTTP/1.1 422 Unprocessable Entity
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
content-type: application/problem+json
Content-Length: 376
Date: Fri, 23 Oct 2020 09:41:15 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"type":"https://stoplight.io/prism/errors#UNPROCESSABLE_ENTITY","title":"Invalid request body payload","status":422,"detail":"Your request is not valid and no HTTP validation response was found in the spec, so Prism is generating this error for you.","validation":[{"location":["query"],"severity":"Error","code":"required","message":"should have required property 'name'"}]}
GET http://127.0.0.1:4010/pets/785
The previous example was an endpoint that didn't require authentication, but this one returns 401 when accessed without credentials.
$ curl -i 'http://127.0.0.1:4010/pets/458' 18:49:12
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"}]
Content-type: application/json
Content-Length: 39
Date: Fri, 23 Oct 2020 09:49:46 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"code":-2147483648,"message":"string"}
As defined under security, adding the api_key header returns a normal response. It seems only the presence of the api_key is checked, not its value.
$ curl -i 'http://127.0.0.1:4010/pets/458' -H 'api_key: key' 18:51:54
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: 126
Date: Fri, 23 Oct 2020 09:52:17 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"id":2,"category":{"id":1,"name":"Felis"},"tags":[{"id":1,"name":"pet"}],"name":"Fluffy","status":"available","photoUrls":[]}
POST http://127.0.0.1:4010/no_auth/pets
The same applies to POST requests: if required fields are defined for the request body, Prism validates them too.
In the sample schema, this endpoint has no 2xx response defined, so it results in an error. The logic that determines the response status and content is explained in detail here.
$ curl -i -XPOST 'http://127.0.0.1:4010/no_auth/pets' -H 'content-type: application/json' -d '{"name": "cat", "photoUrls": []}'
HTTP/1.1 500 Internal Server Error
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
content-type: application/problem+json
Content-Length: 148
Date: Fri, 23 Oct 2020 10:05:32 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"type":"https://stoplight.io/prism/errors#NO_SUCCESS_RESPONSE_DEFINED","title":"No response in the range 200-299 defined","status":500,"detail":""}
- Dynamically changing the response content
Prism has a dynamic option that, when specified, makes it return different content on every request. The values appear to be generated by Faker.js based on the property types defined in the schema.
$ prism mock -d https://raw.githubusercontent.com/stoplightio/prism/master/examples/petstore.oas3.yaml
$ curl 'http://127.0.0.1:4010/no_auth/pets?name=saepe'
[
{
"name": "sin",
"photoUrls": [
"et nostrud labore veniam ut",
"laboris enim",
"pariatur cupidatat ad est ullamco",
"exercitatio",
"non eiusmod laboris",
"aute in",
# Truncated for brevity
Since it's dynamic — or really, random — fine-grained control over the behavior might be difficult. If you define the data types in detail, the generated values will match, so it's worth writing out the schema's Data Types as thoroughly as possible where you can. For example, use type: string plus format: email for an email address, or type: integer plus minimum: 1 for a table ID, and so on.
It would be even more useful if future options allowed finer control over how response data is generated.
Summary
From trying out Prism, it's easy to run, but fine-grained control over the response data seems difficult. Even so, having a mock server that guarantees behavior based on the OpenAPI schema seems like it can meaningfully reduce the extent to which API implementation blocks frontend work.
This is still in the concept stage, so we'd like to keep exploring with our frontend engineers and build an even better development workflow.