DEV Community

John Vester
John Vester

Posted on

FastAPI Got Me an OpenAPI Spec Really... Fast

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!

Top comments (0)