The following describes creating a basic FastAPI using python to manage Beers. The FastAPI has some superb documentation that even the most ardent hungovercoder can understand! I think I’d actually been out on the Thursday and Friday on the beers when coding the below…

PreRequisites

Setup your Codebase

In your favourite IDE setup a folder structure like the following.

beerapi
│   README.md
│   requirements.txt
|   .gitignore
│
└───app
│   │   main.py

In the requirements.txt file add these three rows which will import the libraries we need to create our FastAPI.

fastapi
pydantic
uvicorn

In the .gitignore file ensure your virtual environment is ignored by adding the following line.

venv/*

Setup your Python Environment

When in your project folder root, create a new python virtual environment by running the following in a terminal:

python -m venv venv

Initalise the environment

venv\scripts\activate

Install the required python libraries from the requirements.txt file.

pip install -r requirements.txt

Lets start by saying hello to everyone at the bar…

Hello World

In the app/main.py file enter the following code:

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    """Welcomes you to the beer API

    Returns:
        string: Welcome message
    """
    return "Welcome to the beer API!"

This imports the FastAPI library that we installed from the requirements.txt. It then sets up a new route with the GET method that will return a welcome message at the root path of the api. The name of the app is “app” where it is declared as an instance of the FastAPI class. This then gets utilised with the @app syntax and the GET method to return the welcome message.

To start your app, make sure you are in the same directory as the main.py file, then run the following:

uvicorn main:app --reload

If you open up http://localhost:8000/ in a browser now you should see the welcome message described above. Well done on your fast API beer fellow hungovercoder!

welcome

Craft a Beer Model

We’re going to create a beer model that we can use to interact with in our API. This provides class functionality and validation on the data we are using for our beers. We can do this by creating a class that inherits from the base model of the pydantic library we installed.

First we’ll need to import pydantic and include the BaseModel and Field. We are then going to create a Beer class with three fields of name, brewer and strength, all of them with types. We’re then going to use pyadantics fields to provide some metadata for each property when utilised in the FastAPI. This is going to prove really useful when reading the documentation for our API later. For the beer strength we have also added some validation stating that it must be greater than 0 and less than 100. Another great feature to stop us inputting incorrect values with drunken fingers.

Copy and paste the below code above your FastAPI() initiation code.

from fastapi import FastAPI
from pydantic import BaseModel, Field

class Beer(BaseModel):
    """This is a beer

    Args:
        BaseModel (BaseModel): This is a pydantic base model
    """
    name: str = Field(example="Mike Rayer"
                      ,description="This is the name of the beer")
    brewer: str = Field(example="Crafty Devil"
                        ,description="This is the name of the brewer of the beer")
    strength: float = Field(gt=0,lt=100,example=5.2
                            ,description="This is the strength of the alcohol in the beer")

We’re now going to use this model to create a beer and return it in our API.

GET

We’re going to need to create an example beer in order to GET one. To do this copy and paste the following code after your Beer class code. The reason we’re adding it a list is so we can add it to it with a POST later.

beer_list = []
beer1 = Beer(name="Mike Rayer",brewer="Crafty Devil",strength=4.6)
beer_list.append(beer1)
beer2 = Beer(name="Stay Puft",brewer="Tiny Rebel",strength=4.8)
beer_list.append(beer2)

In order to GET the beers, we need to add the code below after the root function we created earlier.

@app.get("/beers/")
async def get_beers(response: Response):
    """Returns all available beers

    Returns:
        list[Beer]: Returns a list of beer objects
    """
    response.status_code=status.HTTP_200_OK
    return beer_list

This will add a new GET endpoint at beers in the url. The function will take in a response object that allows us to tailor our response status codes (more on this later). The docstring we added after the function will even appear in our documentation. Useful: The docstring VS code add-in for python is really useful for lazily creating docstring stubs…

We’ll also need to update our FastAPI import module at the top of our file to include the following:

from fastapi import FastAPI, status, Response

Now if we run our application again in case it wasn’t running…

uvicorn main:app --reload

We can then go to http://localhost:8000/docs to immediately start looking at the documentation for our API! This makes it really easy to try our new API GET method.

Under the GET method you will see the description you added to the docstring. You will also see a “try it out button”.

GET Try it Out

Click on this button and then click execute. You will see the beer list in the response showing you can test the API works as expected right here in the browser swagger documents out of the box. Very cool.

GET Execute

POST

Now its time to create some beers with a POST request and really put our beer model to work. In order for use the Body methods in FastAPI, you’ll need to update the FastAPI import to include Body as well at the top of your file.

from fastapi import FastAPI, status, Response, Body

Then, after the GET method added in the previous section, add the following code:

@app.post("/beers/")
async def create_beer(response: Response, beer: Beer= Body(
        examples={
            "normal": {
                "summary": "A normal example",
                "description": "A **normal** item works correctly.",
                "value": {
                    "name": "Elvis Juice",
                    "brewer": "Brewdog",
                    "strength": 4.6
                },
            },
             "beerTooStrong": {
                "summary": "Too strong beer fails",
                "description": "This is a beer with a strength above 100%.",
                "value": {
                    "name": "Elvis Juice",
                    "brewer": "Brewdog",
                    "strength": 101.0
                },
            },
            "beerTooWeak": {
                "summary": "Too weak beer fails",
                "description": "This is a beer with a strength below 0%.",
                "value": {
                    "name": "Elvis Juice",
                    "brewer": "Brewdog",
                    "strength": -1.0
                },
            },
            "invalidMissingStrength": {
                "summary": "Beers without a strength are rejected",
                "description": "Beers without a strength are rejected",
                "value": {
                    "name": "Mike Rayer",
                    "brewer": "Crafty Devil"
                },
            },
        },
    )):
    """Creates a new beer

    Args:
        beer (Beer): A beer object with the properties specified in the schemas.

    Returns:
        string: Description of whether beer is added.
    """
    if beer not in beer_list:
        beer_list.append(beer)
        content = f'Beer "{beer.name}" Added.'
        response.status_code=status.HTTP_201_CREATED
        return content

This code creates a new endpoint for POST at beers that allows us to create a beer. The function itself is quite simple and just adds a new beer object to the list we have already created if it does not already exist. It then returns a message stating the beer has been added and a 201 created code.

The new cool stuff happens using the fact we have created a pydantic base model to validate the body of the request as it comes in. This was done earlier when we created the Beer class inherting from the basemodel. This means the body that gets posted to the API will have to meet that Beer class model criteria when it is passed in to the POST request.

As well as this validation, we can also extend the body to include example request and provide scenarios for consumers to try out straight away in the docs. This is what the normal, too strong, too weak and missing strength docs do in this statement. We will see all this working in the documentation for the API.

Now if we run our application again in case it wasn’t running…

uvicorn main:app --reload

We can then go to http://localhost:8000/docs to start looking at our new documentation for our API…

First see that under the methods at the top of the API docs, you will see that there is now a new section called schemas. This includes all the rich documentation we declared in the class model, as we all as any validation rules we declared, such as those on the strength property.

POST Schemas

We can then go into POST beers request and see the metadata here. Under the request body section you will see a dropdown with all the examples we setup in our docstring. If you again go to “try it out” you can try each of these scenarios out and ensure all the expected behaviour occurs.

POST Examples

If you execute a normal example you will see that you get a 200 and the response that the beer is added. If you do a too strong beer example it will fail with a 422 and explain that the strength should be less than 100. If you do too weak beer example it will fail with a 422 and explain that the strength should be more than 100. Finally if you do the beers without a strength you get a 422 because strength is a required property of a beer.

POST Too Strong

If you want to confirm that your new beer was added, you can go to the example GET request and see that you now have three beers instead of the starting two.

Giving the Craft Beer some Flavour with Enums

Right I can never think of any beer flavours so I need some inspiration with enums to inspire me during my beer tasting…

Above your Beer class add the following code to create a new Flavour enum that we will add as an array property to our Beer model. This will allow people to add all the flavours a beer tastes of when adding one so other hungovercoders can look for tastes that they like!

class Flavour(str, Enum):
    """This is a flavour of a beer

    Args:
        str (_type_): _description_
        Enum (_type_): _description_
    """
    HOPPY = "Hoppy"
    CHOCOLATE = "Chocolate"
    CARAMEL = "Caramel"
    ORANGE = "Orange"

To include this in our beer model as an optional field, updated the Beer class to look like the below, with the extra Flavour property.

class Beer(BaseModel):
    """This is a beer

    Args:
        BaseModel (_type_): _description_
    """
    name: str = Field(example="Mike Rayer"
                      ,description="This is the name of the beer")
    brewer: str = Field(example="Crafty Devil"
                        ,description="This is the name of the brewer of the beer")
    strength: float = Field(gt=0,lt=100,example=5.2
                            ,description="This is the strength of the alcohol in the beer")
    flavours: Union[List[Flavour], None] = Field(default=None
                                                 ,example=["Caramel"]
                                                 ,description="These are the lists \
                                                 of flavours in the beer")

The flavours property we add is actually a list of flavours so we can add more than one. We also make it optional by setting the default to None and Unioning the flavour list with a None value. To include the new required features of Union, List and Enum, you’ll need to update your import statements at the top of the file to include the following:

from typing import Union, List
from enum import Enum

We’re also going to add a flavour example to our POST API so its easier for us to see one in action, one with a valid flavour and one with an invalid flavour. In order to do amend your POST method to be as the following:

@app.post("/beers/")
async def create_beer(response: Response, beer: Beer= Body(
        examples={
            "normal": {
                "summary": "A normal example",
                "description": "A **normal** item works correctly.",
                "value": {
                    "name": "Elvis Juice",
                    "brewer": "Brewdog",
                    "strength": 4.6
                },
            },
             "beerTooStrong": {
                "summary": "Too strong beer fails",
                "description": "This is a beer with a strength above 100%.",
                "value": {
                    "name": "Elvis Juice",
                    "brewer": "Brewdog",
                    "strength": 101.0
                },
            },
            "beerTooWeak": {
                "summary": "Too weak beer fails",
                "description": "This is a beer with a strength below 0%.",
                "value": {
                    "name": "Elvis Juice",
                    "brewer": "Brewdog",
                    "strength": -1.0
                },
            },
            "invalidMissingStrength": {
                "summary": "Beers without a strength are rejected",
                "description": "Beers without a strength are rejected",
                "value": {
                    "name": "Mike Rayer",
                    "brewer": "Crafty Devil"
                },
            },
            "beerGoodFlavour": {
                "summary": "Good flavour addition",
                "description": "This is a beer with an allowable list of flavours",
                "value": {
                    "name": "Old Abbot",
                    "brewer": "Hogwarts",
                    "strength": 5.0,
                    "flavours": ["Orange","Caramel"],
                },
            },
            "beerBadFlavour": {
                "summary": "Bad flavour addition",
                "description": "This is a beer with an allowable list of flavours",
                "value": {
                    "name": "Hobgoblin",
                    "brewer": "Wychwood",
                    "strength": 4.3,
                    "flavours": ["Orange","Toothpaste"],
                },
            }
        },
    )):
    """Creates a new beer

    Args:
        beer (Beer): A beer object with the properties specified in the schemas.

    Returns:
        string: Description of whether beer is added.
    """
    if beer not in beer_list:
        beer_list.append(beer)
        content = f'Beer "{beer.name}" Added.'
        response.status_code=status.HTTP_201_CREATED
        return content

Now if we run our application again in case it wasn’t running…

uvicorn main:app --reload

We can then go to http://localhost:8000/docs to start looking at our new documentation for our API…

Under schemas at the bottom we can now see flavour. This describes what a flavour is and shows what the allowable values are. If we look in the Beer model we now also see flavours as a property and it shows the same allowable list of flavours.

ENUM Schema

To test out the validation of this new flavour property we can go to the POST request and try out the examples we added. If we run the good flavour addition we should get a 201 response as the new beer is added. If we run the bad flavour addition we get a 422 as “toothpaste” is not a valid flavour enum!

ENUM Example

To confirm we can see our newly added beer with an optional list of flavours added, go to GET beers, click try it out and execute, and you will see the new beer there.

ENUM GET

Complete Code

Below is the complete code that you will need in a file to run the API. To run this code you just need to run the following in a terminal in the same directory as the file:

uvicorn main:app --reload

Main File

from fastapi import FastAPI, status, Response, Body
from pydantic import BaseModel, Field
from typing import Union, List
from enum import Enum

class Flavour(str, Enum):
    """This is a flavour of a beer

    Args:
        str (_type_): _description_
        Enum (_type_): _description_
    """
    HOPPY = "Hoppy"
    CHOCOLATE = "Chocolate"
    CARAMEL = "Caramel"
    ORANGE = "Orange"

class Beer(BaseModel):
    """This is a beer

    Args:
        BaseModel (_type_): _description_
    """
    name: str = Field(example="Mike Rayer"
                      ,description="This is the name of the beer")
    brewer: str = Field(example="Crafty Devil"
                        ,description="This is the name of the brewer of the beer")
    strength: float = Field(gt=0,lt=100,example=5.2
                            ,description="This is the strength of the alcohol in the beer")
    flavours: Union[List[Flavour], None] = Field(default=None
                                                 ,example=["Caramel"]
                                                 ,description="These are the lists \
                                                 of flavours in the beer")

beer_list = []
beer1 = Beer(name="Mike Rayer",brewer="Crafty Devil",strength=4.6)
beer_list.append(beer1)
beer2 = Beer(name="Stay Puft",brewer="Tiny Rebel",strength=4.8)
beer_list.append(beer2)

app = FastAPI()

@app.get("/")
async def root():
    """Welcomes you to the beer API

    Returns:
        string: Welcome message
    """
    return "Welcome to the beer API!"

@app.get("/beers/")
async def get_beers(response: Response):
    """Returns all available beers

    Returns:
        list[Beer]: Returns a list of beer objects
    """
    response.status_code=status.HTTP_200_OK
    return beer_list

@app.post("/beers/")
async def create_beer(response: Response, beer: Beer= Body(
        examples={
            "normal": {
                "summary": "A normal example",
                "description": "A **normal** item works correctly.",
                "value": {
                    "name": "Elvis Juice",
                    "brewer": "Brewdog",
                    "strength": 4.6
                },
            },
             "beerTooStrong": {
                "summary": "Too strong beer fails",
                "description": "This is a beer with a strength above 100%.",
                "value": {
                    "name": "Elvis Juice",
                    "brewer": "Brewdog",
                    "strength": 101.0
                },
            },
            "beerTooWeak": {
                "summary": "Too weak beer fails",
                "description": "This is a beer with a strength below 0%.",
                "value": {
                    "name": "Elvis Juice",
                    "brewer": "Brewdog",
                    "strength": -1.0
                },
            },
            "invalidMissingStrength": {
                "summary": "Beers without a strength are rejected",
                "description": "Beers without a strength are rejected",
                "value": {
                    "name": "Mike Rayer",
                    "brewer": "Crafty Devil"
                },
            },
            "beerGoodFlavour": {
                "summary": "Good flavour addition",
                "description": "This is a beer with an allowable list of flavours",
                "value": {
                    "name": "Old Abbot",
                    "brewer": "Hogwarts",
                    "strength": 5.0,
                    "flavours": ["Orange","Caramel"],
                },
            },
            "beerBadFlavour": {
                "summary": "Good flavour addition",
                "description": "This is a beer with an allowable list of flavours",
                "value": {
                    "name": "Hobgoblin",
                    "brewer": "Wychwood",
                    "strength": 4.3,
                    "flavours": ["Orange","Toothpaste"],
                },
            }
        },
    )):
    """Creates a new beer

    Args:
        beer (Beer): A beer object with the properties specified in the schemas.

    Returns:
        string: Description of whether beer is added.
    """
    if beer not in beer_list:
        beer_list.append(beer)
        content = f'Beer "{beer.name}" Added.'
        response.status_code=status.HTTP_201_CREATED
        return content