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 articlesGET
/articles/{article_id}
– will retrieve a single article by the id propertyPOST
/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!