Build APIs safely and interactively

Valentin Mouret
4 min readMar 16, 2023

--

Generated by MidJourney

APIs (Application Programming Interface)s are everywhere today. But how do we build them exactly?

I wanted to have a look at the tooling to make the development of the API fast, conform to the specs, and resilient to code changes.

What I used:

I am curious to know what you would do differently, so please share your tips and feedback!

API

An API is centered around a contract. A document we settled on with the various users of the API. People are going to rely on this spec, so we should seldom change it in the future. If we do, it’s with versioning, so that we never break the contract.

As a toy example, I use the classic petstore. Let’s pretend that’s the contract we have toward our users.

API server

The API server is the program that is going to be accessed by our users via HTTP and serve the information they expect. It’s entirely up to you what you make this server with, and that’s the beauty of interfacing.

For the purpose of this article, I will use FastAPI. This is a Python library that looks like Flask but leverages type hints to optimize the code.

Feel free to use any other tech here.

Setup

The only dependency you need here is Python. I also use Poetry to manage the project, highly recommend it.

poetry new petstore # Creates a new Poetry project
cd petstore
poetry add "fastapi[all]"
poetry install

Our source code is going to be inside petstore/petstore. Let’s create a server.py that’s going to contain all of our server code.

You can run the server with: poetry run unicorn petstore.server:app --reload (by default, it will run on port 8000).

You can check it works fine with: http get localhost:8000 (http comes from httpie, you can also use curl).

You should see something like:

HTTP/1.1 200 OK
content-length: 26
content-type: application/json
server: uvicorn
{
"message": "hello world!"
}

Our server is up, let’s get cracking on the API then!

Specs

You can copy the specs into your project: http get https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/examples/v2.0/yaml/petstore.yaml > spec.yaml.

Now let’s read our specs, starting with the model definitions:

definitions:
Pet:
type: "object"
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
Pets:
type: array
items:
$ref: '#/definitions/Pet'

This is fairly easy to understand. But, I would feel more comfortable if I had a program checking my API server against the spec. And that’s what Prism is here for!

Prism

Prism is a bundle of tools to help build APIs. Today, I am interested in the proxy server.

Instead of hitting our API directly, we will hit the proxy which is going to hit our API. If the request or response is not conform to the spec, the proxy will return an HTTP error.

You can install Prism with npm: npm install @stoplight/prism-cli.

Then, you can run it with npx: npx prism proxy spec.yaml http://localhost:8000 --errors.

And now, check our endpoint matches the spec (note that we hit the proxy and not the server directly):

> http get localhost:4010/pets
[
{
"id": 0,
"name": "cat",
"tag": "lazy"
},
{
"id": 1,
"name": "dog",
"tag": "good"
},
{
"id": 2,
"name": "llama",
"tag": "unusual"
}
]

Well, looks like we match the spec! Let’s try the /pets/{pet_id} endpoint.

That was easy! Let’s test it:

❯ http get localhost:4010/pets/0 | jq
{
"type": "https://stoplight.io/prism/errors#VIOLATIONS",
"title": "Request/Response not valid",
"status": 500,
"detail": "Your request/response is not valid and the --errors flag is set, so Prism is generating this error for you.",
"validation": [
{
"location": [
"response",
"body"
],
"severity": "Error",
"code": "type",
"message": "must be array"
}
]
}

Oh no! Looks like I was too cocky. Apparently, the spec expects /pets/{pet_id} to return a list of pets. That’s weird, but that’s the spec, so that’s what our users expect. Let’s fix it, and test it:

❯ http get localhost:4010/pets/0 | jq
[
{
"id": 0,
"name": "cat",
"tag": "lazy"
}
]

There we go!

Wouldn’t it be nice if we could reuse these HTTP requests and the prism proxy to test our API against the spec in the future?
Let’s build regression tests!

Regression tests

The concept is simple: if nothing changes (we did not break anything in our codebase), the same inputs should have the same outputs.
We are going to record HTTP queries and the outputs we get and compare them against «expected» outputs.

I made a bash script to make things simpler:

  • our queries are under tests/queries
  • the results of these queries against the current system are under tests/out
  • the expected results are under tests/expected

To test our API: ./regress test

If everything is fine, we can attest that the current output is valid: ./regress attest .

This can be easily added to a CI/CD, so that we check our code did not break the contract.

Conclusion

With little tooling, we were able to build an environment that makes it simple, fast, and safe to build our API server to match the spec.

The good thing as well is that it’s language agnostic.
It does not depend on the tools you chose to build your API server.

If you do things differently or if you have any feedback on this method, I would love to hear from you!

--

--