FastAPI Got Me an OpenAPI Spec Really... Fast
When API First isn’t an option, FastAPI can save teams time by allowing existing RESTful microservices to be fully documented and consumed using OpenAPI v3 automatically.
Join the DZone community and get the full member experience.
Join For FreeReaders 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 myself 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 all of my updates in 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!
Opinions expressed by DZone contributors are their own.
Comments