FastAPI Got Me an OpenAPI Spec Really... Fast
Tags: python, fastapi, heroku, openapi
Published: 2024-04-22 18:28:55


![article image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fg0crwb0ampyhntzhsrq.jpg)

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](https://dzone.com/articles/stack-overflow-driven-development-sodd-its-really) (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](https://fastapi.tiangolo.com/).

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:

```plaintext
$ pip install --upgrade pip
$ pip install fastapi
$ pip install uvicorn
```

I created a new Python file called [`api.py`](http://api.py) and added some imports, plus established an `app` variable:

```python
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:

```python
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:

```python
# 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:

```python
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:

```plaintext
$ 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`):

```plaintext
$ 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](https://www.heroku.com/home) 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`:

```plaintext
$ pip install pipreqs
$ pipreqs
```

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

```plaintext
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:

```plaintext
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](https://devcenter.heroku.com/articles/getting-started-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:

```plaintext
$ 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:

```plaintext
$ 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:

```plaintext
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:

```plaintext
$ git remote
heroku
origin
```

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

```plaintext
$ 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:

![Image 1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v1f5qzuj8ywwd800fxjj.png)

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

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

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

```json
{
    "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:

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

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

![Image 2](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hp4yu8mflfuih3390byj.png)

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

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

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

```json
{
  "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](https://openapi-generator.tech/).

## **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](https://gitlab.com/johnjvester/fast-api-demo).

Have a really great day!