FastAPI Got Me an OpenAPI Spec Really… Fast

John Vester
Level Up Coding
Published in
8 min readApr 24, 2024

--

Readers of my publications are likely familiar with the idea of employing an API First approach to developing microservices. Countless times I have realized the benefits of describing the anticipated URIs and underlying object models before any development begins.

In my 30+ years of navigating technology, however, I’ve come to expect the realities of alternate flows. In other words, I fully expect there to be situations where API First is just not possible.

For this article, I wanted to walk through an example of how teams producing microservices can still be successful at providing an OpenAPI specification for others to consume without manually defining an openapi.json file.

I also wanted to step outside my comfort zone and do this without using Java, .NET, or even JavaScript.

Discovering FastAPI

At the conclusion of most of my articles I often mention my personal mission statement:

“Focus your time on delivering features/functionality that extends the value of your intellectual property. Leverage frameworks, products, and services for everything else.” — J. Vester

My point in this mission statement is to make myself accountable for making the best use of my time when trying to reach goals and objectives set at a higher level. Basically, if our focus is to sell more widgets, my time should be spent finding ways to make that possible — steering clear of challenges that have already been solved by existing frameworks, products, or services.

I picked Python as the programming language for my new microservice. To date, 99% of the Python code I’ve written for my prior articles has been the result of either Stack Overflow Driven Development (SODD) or ChatGPT-driven answers. Clearly, Python falls outside my comfort zone.

Now that I’ve level-set where things stand, I wanted to create a new Python-based RESTful microservice that adheres to my personal mission statement with minimal experience in the source language.

That’s when I found FastAPI.

FastAPI has been around since 2018 and is a framework focused on delivering RESTful APIs using Python-type hints. The best part about FastAPI is the ability to automatically generate OpenAPI 3 specifications without any additional effort from the developer’s perspective.

The Article API Use Case

For this article, the idea of an Article API came to mind, providing a RESTful API that allows consumers to retrieve a list of my recently published articles.

To keep things simple, let’s assume a given Article contains the following properties:

  • id — simple, unique identifier property (number)
  • title — the title of the article (string)
  • url— the full URL to the article (string)
  • year — the year the article was published (number)

The Article API will include the following URIs:

  • GET /articles — will retrieve a list of articles
  • GET /articles/{article_id} — will retrieve a single article by the id property
  • POST /articles — adds a new article

FastAPI In Action

In my terminal, I created a new Python project called fast-api-demo and then executed the following commands:

$ pip install --upgrade pip
$ pip install fastapi
$ pip install uvicorn

I created a new Python file called api.py and added some imports, plus established an app variable:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)

Next, I defined an Article object to match the Article API use case:

class Article(BaseModel):
id: int
title: str
url: str
year: int

With the model established, I needed to add the URIs … which turned out to be quite easy:

# Route to add a new article
@app.post("/articles")
def create_article(article: Article):
articles.append(article)
return article

# Route to get all articles
@app.get("/articles")
def get_articles():
return articles

# Route to get a specific article by ID
@app.get("/articles/{article_id}")
def get_article(article_id: int):
for article in articles:
if article.id == article_id:
return article
raise HTTPException(status_code=404, detail="Article not found")

To save me from involving an external data store, I decided to add some of my recently published articles programmatically:

articles = [
Article(id=1,
title="Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services",
url="https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", year=2023),
Article(id=2, title="Using Unblocked to Fix a Service That Nobody Owns",
url="https://dzone.com/articles/using-unblocked-to-fix-a-service-that-nobody-owns", year=2023),
Article(id=3, title="Exploring the Horizon of Microservices With KubeMQ's New Control Center",
url="https://dzone.com/articles/exploring-the-horizon-of-microservices-with-kubemq", year=2024),
Article(id=4, title="Build a Digital Collectibles Portal Using Flow and Cadence (Part 1)",
url="https://dzone.com/articles/build-a-digital-collectibles-portal-using-flow-and-1", year=2024),
Article(id=5, title="Build a Flow Collectibles Portal Using Cadence (Part 2)",
url="https://dzone.com/articles/build-a-flow-collectibles-portal-using-cadence-par-1", year=2024),
Article(id=6,
title="Eliminate Human-Based Actions With Automated Deployments: Improving Commit-to-Deploy Ratios Along the Way",
url="https://dzone.com/articles/eliminate-human-based-actions-with-automated-deplo", year=2024),
Article(id=7, title="Vector Tutorial: Conducting Similarity Search in Enterprise Data",
url="https://dzone.com/articles/using-pgvector-to-locate-similarities-in-enterpris", year=2024),
Article(id=8, title="DevSecOps: It's Time To Pay for Your Demand, Not Ingestion",
url="https://dzone.com/articles/devsecops-its-time-to-pay-for-your-demand", year=2024),
]

Believe it or not, that completes the development for the Article API microservice.

For a quick sanity check, I spun up my API service locally:

$ python api.py
INFO: Started server process [320774]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)

Then, in another terminal window, I sent a curl request (and piped it to json_pp):

$ curl localhost:8000/articles/1 | json_pp
{
"id": 1,
"title": "Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services",
"url": "https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste",
"year": 2023
}

Preparing to Deploy

Rather than just run the Article API locally, I thought I would see how easily I could deploy the microservice. Since I had never deployed a Python microservice to Heroku before, I felt like now would be a great time to try.

Before diving into Heroku, I needed to create a requirements.txt file to describe the dependencies for the service. To do this, I installed and executed pipreqs:

$ pip install pipreqs
$ pipreqs

This created a requirements.txt file for me, with the following information:

fastapi==0.110.1
pydantic==2.6.4
uvicorn==0.29.0

I also needed a file called Procfile which tells Heroku how to spin up my microservice with uvicorn. Its contents looked like this:

web: uvicorn api:app --host=0.0.0.0 --port=${PORT}

Let’s Deploy to Heroku

For those of you who are new to Python (as I am), I used the Getting Started on Heroku with Python documentation as a helpful guide.

Since I already had the Heroku CLI installed, I just needed to log in to the Heroku ecosystem from my terminal:

$ heroku login

I made sure to check in all of my updates into my repository on GitLab.

Next, the creation of a new app in Heroku can be accomplished using the CLI via the following command:

$ heroku create

The CLI responded with a unique app name, along with the URL for app and the git-based repository associated with the app:

Creating app... done, powerful-bayou-23686
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/ |
https://git.heroku.com/powerful-bayou-23686.git

Please note — by the time you read this article, my app will no longer be online.

Check this out. When I issue a git remote command, I can see that a remote was automatically added to the Heroku ecosystem:

$ git remote
heroku
origin

To deploy the fast-api-demo app to Heroku, all I have to do is use the following command:

$ git push heroku main

With everything set, I was able to validate that my new Python-based service is up and running in the Heroku dashboard:

With the service running, it is possible to retrieve the Article with id = 1 from the Article API by issuing the following curl command:

$ curl --location 'https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/articles/1'

The curl command returns a 200 OK response and the following JSON payload:

{
"id": 1,
"title": "Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services",
"url": "https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste",
"year": 2023
}

Delivering OpenAPI 3 Specifications Automatically

Leveraging FastAPI’s built-in OpenAPI functionality allows consumers to receive a fully functional v3 specification by navigating to the automatically generated /docs URI:

https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/docs

Calling this URL returns the Article API microservice using the widely adopted Swagger UI:

For those looking for an openapi.json file to generate clients to consume the Article API, the /openapi.json URI can be used:

https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/openapi.json

For my example, the JSON-based OpenAPI v3 specification appears as shown below:

{
"openapi": "3.1.0",
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"paths": {
"/articles": {
"get": {
"summary": "Get Articles",
"operationId": "get_articles_articles_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {

}
}
}
}
}
},
"post": {
"summary": "Create Article",
"operationId": "create_article_articles_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Article"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {

}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/articles/{article_id}": {
"get": {
"summary": "Get Article",
"operationId": "get_article_articles__article_id__get",
"parameters": [
{
"name": "article_id",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"title": "Article Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {

}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Article": {
"properties": {
"id": {
"type": "integer",
"title": "Id"
},
"title": {
"type": "string",
"title": "Title"
},
"url": {
"type": "string",
"title": "Url"
},
"year": {
"type": "integer",
"title": "Year"
}
},
"type": "object",
"required": [
"id",
"title",
"url",
"year"
],
"title": "Article"
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
}
}
}
}

As a result, the following specification can be used to generate clients in a number of different languages via OpenAPI Generator.

Conclusion

At the start of this article I was ready to go to battle and face anyone not interested in using an API First approach. What I learned from this exercise is that a product like FastAPI can help define and produce a working RESTful microservice quickly while also including a fully consumable OpenAPI v3 specification … automatically.

Turns out, FastAPI allows teams to stay focused on their goals and objectives by leveraging a framework that yields a standardized contract for others to rely on. As a result, another path has emerged to adhere to my personal mission statement.

Along the way, I used Heroku for the first time to deploy a Python-based service. This turned out to require little effort on my part, other than reviewing some well-written documentation. So another mission-statement bonus needs to be mentioned for the Heroku platform as well.

If you are interested in the source code for this article you can find it on GitLab.

Have a really great day!

--

--

Information Technology professional with 25+ years expertise in application architecture, design and development. Agile project and team management.