FastAPI Got Me an OpenAPI Spec Really... Fast

John Vester - Apr 22 - - Dev Community

article image

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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

class Article(BaseModel):
    id: int
    title: str
    url: str
    year: int
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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),
]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

fastapi==0.110.1
pydantic==2.6.4
uvicorn==0.29.0
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

$ git push heroku main
Enter fullscreen mode Exit fullscreen mode

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

Image 1

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'
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Image 2

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
Enter fullscreen mode Exit fullscreen mode

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"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .