The following describes creating a basic Flask API using python to manage Beers. Mmm beer… in a flask…

PreRequisites

  • You’ll need python installed.
  • You’ll need a decent IDE - I use visual studio code.
  • As we’re playing with APIs, you’ll also need to install Postman to make this process much easier.
  • Ideally you should have git installed.

Setup your Codebase

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

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

In the requirements.txt file add these two rows which will import the libraries we need to create our Flask API.

Flask
flask_expects_json

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 making beer!

Hello World

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

from flask import Flask

app = Flask(__name__)

@app.route('/hello/', methods=['GET'])
def hello():
    return "Hello and welcome to to the beer API!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=105, debug=True)

This imports the flask 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 path “hello”. The app route syntax above the hello function sets an expectation of only a GET method at this endpoint so it will reject any other methods. Finally it initializes the application running on local host port 105 with debug set to true. This debug setting you would not have in production but it sure makes it easier to experiment with locally whilst supping on a few cans.

To start your app run the following:

app\app.py

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

hello world

GET

Well I’m getting thirsty so lets start getting some beers…

First at the top of your app.py file add some further imports. This will allow us to “jsonify” the request methods as well as access request properties utilising flask.

from flask import Flask, jsonify, request

Then above your hello function, add this block of code which will start us up some beer data to read every time we start our app. This is just an array with a simple schema defining the name, brewer and strength of our beers. Ultimately you’d want to consider storing this information in some permanent data storage, but this will work fine for our demo. Just note every time you start the API for this setup all the data will disappear except for this original beer list.

beer_list = [
    {
            "name": "Mike",
            "brewer": "Crafty Devil",
            "strength": "4.2"
    }
]

After the hello method we’ll now want to add a beers endpoint.

@app.route('/beers/', methods=['GET'])
def beers():
    if request.method == 'GET':
        return jsonify(beer_list)

If you now navigate to http://localhost:105/beers/ in a browser you should see your tasty beers.

get beers

It’s important to note the fact we’re interacting with beers, plural, with every method we now implement being considered from the perspective of the consumer interacting with tasty beers. This will follow good REST design principals and I recommend having a read through of the restapitutorial for more resources on the matter.

It’s worthwhile at this point setting up a GET request in Postman to make it easier to test as we’ll be doing this with our following request types. Open Postman, add a new request called “Get Beers” with the URL http://localhost:105/beers/. Hit send and you should see your beers come back in the request response.

postman get

POST

In this new world beyond that of Carling and Worthingtons, we need a way of adding more beers with the POST method.

For this we’re going to perform some schema validation with flask_expects_json. This ensures the beer being added to our beers list is as expected and not some cheap knockoffs.

At the top of your app file add these imports:

from flask_expects_json import expects_json

then above your beer_list, add a beer schema like so.

beer_schema = {
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "brewer": { "type": "string" },
    "strength": { "type": "string" },
  },
  "required": ["name","brewer"]
}

This will ensure that the beer that gets added to our list will always have a name and brewer, but may not always have a strength. I mean often times we just need a beer, any beer, regardless of strength… right?

Now update your beers method to look like the following:

@app.route('/beers/', methods=['GET','POST'])
@expects_json(beer_schema, ignore_for=['GET'])
def beers():
    if request.method == 'GET':
        return jsonify(beer_list)

    if request.method == 'POST':
        new_beer = request.json
        if new_beer not in beer_list:
            beer_list.append(new_beer)
            content = 'Beer Added'
        else:
            content = 'Beer Already Exists'

        return content

At the start of the endpoint, you can see the beers route now expects either a GET or a POST request. With our schema validation using expects_json, which we only require for the POST of data, so we ignore this for the GET. We also pass in the beer schema to ensure that for the POST request we only get request bodies that meet our expected beer schema. The POST method then goes on to setting a new beer from the request and adding this to our beer list if it does not already exist. We then create some simple context messages that we pass back to the API consumer to let them know what has taken place.

Now we need to open Postman and construct ourselves a POST request. To do this create a new request called “Add a Beer”. Then change the request to a POST and add the url http://localhost:105/beers/. Then in the body add the following JSON for a valid beer addition.

{
  "name": "Elvis Juice",
  "brewer": "Brewdog",
  "strength": "5.2"
}

Your full Postman setup to add a beer should look something like this.

postman post

Hit send and you should get a response saying Beer “Elvis Juice Added”. If you hit send again you will get a response saying “Beer Elvis Juice Already Exists”. To confirm that both the original beer and the new beer both exist in our beers, run your GET beers request again in Postman and you should see them both there.

postman get beers

PUT

Sometimes us hungovercoders may slur our words a little… we better find a way of correcting those beers with the PUT method!

To do this we’re going to put the following code below our original beers endpoint. This will allow us to interact with individual beers.

@app.route('/beers/<name>', methods=['GET','PUT'])
@expects_json(beer_schema, ignore_for=['GET'])
def beer(name):
    try:
        beer = [beer for beer in beer_list if beer['name'].replace(' ','').lower() == name.lower()][0]
    except:
         return "Beer does not exist"
    if request.method == 'GET':
        return jsonify(beer)
    if request.method == 'PUT':
        for idx, item in enumerate(beer_list):
            if beer["name"] in item["name"]:
                updated_beer = request.json
                beer_list[idx] = updated_beer
        return "Beer updated"

We again define a route in to the API, except this time we have ‘name’ path in it which allows us to pass in beer names to our requests. We can use this in our functions. First of all we try and find the beer name in our beer list. If we cannot find this beer then we can go no further so we just let the caller know that this beer does not exist. This likely not the best way of doing it, but you’ll notice in the code we just remove the spaces in the beer name so that we can find it in our beer list. We then implement a GET method to return the individual beer we have requested based on the name, so this is a specific GET functionality. In order to perform a PUT we do some funky iteration over a beer list and update the original beer with a brand new beer than has been sent in the body of the PUT request. Just like in the original beers POST and GET, we validate the schema for the PUT but we don’t for the GET.

Now we need to open Postman and construct ourselves a new GET and PUT request. To do this for the GET create a new request called “Get a Beer”. Then change the request to be the url http://localhost:105/beers/ElvisJuice.

postman get beer

To construct ourselves a PUT request we need to create a new request called “Update a Beer”. Then change the request to a PUT and add the url http://localhost:105/beers/ElvisJuice. Then in the body add the following JSON for a valid beer strength updated from 5.2 to 5.3.

{
  "name": "Elvis Juice",
  "brewer": "Brewdog",
  "strength": "5.3"
}

postman put beer

Hit send and you should get a response saying Beer “Beer ElvisJuice Updated”. To confirm that the beer has been updated, run your GET beer for ElvisJuice again in Postman and you should see the updated strength value.

postman get beer updated

DELETE

I don’t know why anyone would ever want to remove a beer from this world… but here goes. Sigh.

As part of our specific beer name endpoint we now want to allow the DELETE method, with some adjustments to the PUT method to cater for reusing the index on the DELETE method.

@app.route('/beers/<name>', methods=['GET','PUT','DELETE'])
@expects_json(beer_schema, ignore_for=['GET','DELETE'])
def beer(name):
    try:
        beer = [beer for beer in beer_list if beer['name'].replace(' ','').lower() == name.lower()][0]
    except:
        return f'Beer "{name}" does not exist.'

    if request.method == 'GET':
        return jsonify(beer)

    if request.method in ('PUT','DELETE'):
        for idx, item in enumerate(beer_list):
            if beer["name"] in item["name"]:
                beer_index = idx

        if request.method == 'PUT':
            updated_beer = request.json
            beer_list[beer_index] = updated_beer
            return f'Beer "{name}" updated.'

        if request.method == 'DELETE':
            if beer in beer_list:
                del beer_list[beer_index]
                return f'Beer "{name}" removed.'

We first allow the DELETE method as part of our named beers endpoint. We then remove the need to validate the JSON schema for a DELETE command, as we will just perform a DELETE based on the beers name. Within the endpoint for the named beer then, if the request is PUT or DELETE, we capture the index of the beer. This is something we specifically did for PUT originally but now we do it for both PUT and DELETE. This is so we can update a particular beer at an index point with a PUT, and also remove that beer at the index point with a DELETE. The PUT now as before replaces the original beer at the appropriate index point whilst the DELETE removes the beer at that index point.

To construct ourselves a DELETE request we need to create a new request called “Remove a Beer” in Postman. Then change the request to a DELETE and add the url http://localhost:105/beers/ElvisJuice. If we hit send then this should remove the beer and we will get a message telling us as such.

postman delete beer

Complete Code

Just to ensure all the beers have been put together correctly, below is the full working code for your beer api.

from flask import Flask, jsonify, request
from flask_expects_json import expects_json

app = Flask(__name__)

beer_schema = {
  "type": "object",
  "properties": {
    "name": { "type": "string" },
    "brewer": { "type": "string" },
    "strength": { "type": "string" },
  },
  "required": ["name","brewer"]
}

beer_list = [
    {
            "name": "Mike",
            "brewer": "Crafty Devil",
            "strength": "4.2"
    }
]

@app.route('/hello/', methods=['GET'])
def hello():
    return 'Hello and welcome to to the beer API!'

@app.route('/beers/', methods=['GET','POST'])
@expects_json(beer_schema, ignore_for=['GET'])
def beers():
    if request.method == 'GET':
        return jsonify(beer_list)

    if request.method == 'POST':
        new_beer = request.json
        beer_name = new_beer['name']
        if new_beer not in beer_list:
            beer_list.append(new_beer)
            content = f'Beer "{beer_name}" Added.'
        else:
            content = f'Beer "{beer_name}" Already Exists.'

    return content

@app.route('/beers/<name>', methods=['GET','PUT','DELETE'])
@expects_json(beer_schema, ignore_for=['GET','DELETE'])
def beer(name):
    try:
        beer = [beer for beer in beer_list if beer['name'].replace(' ','').lower() == name.lower()][0]
    except:
        return f'Beer "{name}" does not exist.'

    if request.method == 'GET':
        return jsonify(beer)

    if request.method in ('PUT','DELETE'):
        for idx, item in enumerate(beer_list):
            if beer["name"] in item["name"]:
                beer_index = idx

        if request.method == 'PUT':
            updated_beer = request.json
            beer_list[beer_index] = updated_beer
            return f'Beer "{name}" updated.'

        if request.method == 'DELETE':
            if beer in beer_list:
                del beer_list[beer_index]
                return f'Beer "{name}" removed.'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=105, debug=True)

To celebrate our first beer related hungovercoders blog post, I think it’s time for a song because beer is great