One aspect of dev work that usually has friction is between API engineers and frontend developers. The frontend developer usually has to wait for an API endpoint to be finished before he can integrate it into his work. Even after he has done so, the API engineer might change the endpoint and the frontend developer has to revisit the integration. This leads to wasted time re-coding and more frustrated frontend developers.
Recap
In the previous post, we looked at the API-First Design process and applied the concepts that we learned to our simple loyalty application. We also produced an API sketch that listed each endpoint and top-level details about them.
This Post
In this post, we take our API sketch and turn it into an OpenAPI 3.0 definition. This document describes in finer detail how each of our endpoints operates: what the properties should be in the requestBody/responseBody, should there be query or path parameters, etc.
By having both API engineers and frontend developers start with planning the API, a lot of thought about the frontend-backend interaction has been done upfront. The API definition serves as a "contract" between both developers on how the API should operate. By working on this "contract" before any development, we minimize changes to the API and it guides how both developers implement their projects.
[1] Let's dive right in! 🌊
Before we get bogged down in too much concept, let's get our hands dirty writing OpenAPI definitions. On a separate tab, open up the Swagger editor in your browser. The swagger editor allows you to conveniently create OpenAPI definitions using your browser.
(1.1)
Empty the default contents of the left side of the Swagger editor and place this snippet instead:
openapi: 3.0.0
info:
title: Loyalty Card API
version: "0.1"
paths: {}
The snippet declares the version of the OpenAPI definition and some top-level information about your API. We intentionally left the paths
key to have an empty value so we don't have an error.
As you type the visual documentation on the right updates. It also informs you of syntax errors in your API definition. Right now, it should look like this:
(1.2)
Next, let's define schemas for our API. As we learned in the previous post, standard endpoints define a consistent interface centered around resources in your API (i.e Create a Transaction, Read one transaction, Read all transactions, Edit transaction, etc).
In the schema section, we define how these resources look like: what properties it has and what data type each property is. At this point, it might be tempting to say that the resource should look identical to the database schema. It is not. We can omit some attributes (i.e we opt not to show each transaction's approval_code
) or even have the transaction resource not represented by a transaction table altogether.
While at the start the transaction API and the transaction database table might be similar, over time the divergence between them can be quite huge.
components:
schemas:
Transaction:
type: object
properties:
id:
type: string
amount:
type: number
equivalent_points:
type: number
card_id:
type: string
partner_id:
type: string
Your API should now look like this:
(1.3)
Now, let's get to the meat 🥩 of the API definitions: paths
. Paths define the URL paths of our API endpoints. Let's replace the line paths: {}
with the code snippet below.
# remember to replace paths: {}
paths:
/transactions:
post:
summary: create transaction
tags:
- Transactions
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
responses:
'201':
description: Created user
headers:
Location:
schema:
type: string
In the code snippet, we defined a path: POST /transactions
to create a transaction.
-
summary
- A short info about the endpoint -
tags
- The documentation of the right-side side -
requestBody
- For each type of content, we can define a different schema. In this example, we have the application/json content -
responses
- For each type of response type (HTTP 201 is "object created", HTTP 500 is "internal server error", and so on), we can define different response formats. In this example, if the requests results in HTTP 201, we return an empty response with the header "Location"
(1.4)
Next, we define the GET /transactions
path. Make sure to add this snippet right below the code snippet in 1.3.
# add this under paths
# add this under /transactions
get:
summary: get all transactions
tags:
- Transactions
parameters:
- name: "partner_id"
in: "query"
description: ""
required: false
schema:
type: integer
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/Transaction'
This code snippet is a bit different. Instead of defining a request body, we defined a query parameter instead: /transactions?partner_id=10
. The query parameter allows your frontend to get all transactions posted by a specific partner.
Since this is a GET request, we expect to get something in return. In the responses, we see that if the request is successful (HTTP 200), we return an array of transaction objects.
(1.5)
Once we can create transactions (1.3) and view all transactions (1.4), we need to have a way to view individual transactions. In the code snippet below, that's exactly what we are doing:
# add this under paths
/transactions/{id}:
parameters:
- schema:
type: integer
name: id
in: path
required: true
get:
summary: View Transaction
tags:
- Transactions
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Transaction'
In this snippet, we used a path parameter instead to define the id of the transaction we are viewing. The response is similar to 1.4 except that it instead returns a single object instead of an array of objects.
The right side of our editor should now look like this:
At this point, you might be wondering why I didn't include all the other resources in the API sketch. That would make the blog post very large. I chose to instead focus on the transaction resource. I leave creating the API definition for the other resources as a homework for you. If you get stuck or just want a quick reference, the full Open API 3.0 definition for the simple loyalty application is found at this Github Gist
[2] Mock Server with Prism
In this step, we will use the API definition we created in step 1 to run a mock server locally. For this, we will use Prism, a NodeJS CLI utility to run mock servers.
(2.1)
First, install prism.
npm install --global @stoplight/prism-cli
(2.2)
Save the file locally as "blog_api.oas.yml" and cd
to that directory in your command line. We added the .oas extension to signify that it is a YAML file in the OpenAPI format.
(2.3)
Now, let's run the Prism mock server:
prism mock -p 8080 ./blog_api.oas.yml
Your CLI should look like this:
Now, trying typing this on your browser: http://127.0.0.1:8080/transactions/495
. You should see a very basic response:
You now have a fully operational mock server at your disposal. No need to wait for the API engineer to develop the business logic: your frontend developer can get started coding the frontend right away!
[3] Additional Perks
If that wasn't enough to convince you to do API-First Design, here are a few more perks. These perks are features of the Swagger editor that we used in the first 2 steps.
[1] Pre-generate your backend
API engineers can pre-generate their whole API backend system using just the API definition. This saves a lot of time setting up paths and routing for the application.
The generated application does not come with business logic. It is composed of stubbed routes that return sample responses. But this scaffolding goes a long way in getting you up and running quickly
[2] Pre-generate an SDK
Your API engineers can also pre-generate an SDK (Software Development Kit). SDKs help your end-users interact with your system by providing a software package instead of directly calling your API endpoints. This saves them the boilerplate work of validating request/responses, handling error codes, etc.
The SDK package generated will not be ready for use by your end-users. You will still have to do some more coding. But at least it gets you halfway there already.
Finish!
With our OpenAPI 3.0 Definition, we have created a mock server that allows our frontend developer to integrate API endpoints without waiting for the API engineer to finish the business logic. The dependency between them is minimized and harmony is restored with the team!
We also looked at code pre-generation as a way to help the API engineers create the API backend and SDK faster.
Special Thanks
Special thanks to Allen for making my posts more coherent. This blog post is also made possible by the authors below who have made learning APIs a joy.
- API Design Patterns by JJ Geewax
- Designing APIs with Swagger and OpenAPI by Joshua S. Ponelat and Lukas L. Rosenstock
- Design and Build Great Web APIs by Mike Amundsen
Photo by Julia Joppien on Unsplash
Top comments (0)