DEV Community

Cover image for Build and Deploy a REST API Microservice with Python Flask and Docker
Swarnim Walavalkar
Swarnim Walavalkar

Posted on

Build and Deploy a REST API Microservice with Python Flask and Docker

Objectives:

  • Build a simple but real-world useable REST API
  • Follow REST and Microservice Arch Best Practices
  • Deploy to a Docker Container

Technologies used:

  • Python
  • Flask
  • Flask Restful
  • Docker

Prerequisites:

  • A Foundational Understanding of Python
  • Acquaintance with the Flask Microframework
  • A Primary Understanding of Docker and Docker Container Management

Difficulty: ⚡⚡Intermediate

Setup:

Make sure you have pipenv installed.

pip install pipenv
Enter fullscreen mode Exit fullscreen mode

Then, start by making a directory for the project and cd into it

mkdir FlaskBookApi/
cd FlaskBookApi/
Enter fullscreen mode Exit fullscreen mode

Then install the flask and flask-restful packages using pipenv. This will do a couple of things:

pipenv install flask flask-restful
Enter fullscreen mode Exit fullscreen mode

It creates a python virtual environment with your directory's name in a central location and installs the specified packages (in this case, flask and flask-restful). Along with that, In your working directory, it creates the Pipfile, which contains all of the projects dependencies with their appropriate version numbers along with the Python version used by the project. It also creates the Pipfile.lock to enable deterministic builds in production.

This isn't a pipenv tutorial, So I won't go into too much detail about that.

Write The API:

Alright, Now the fun part! Lets Build this thing!

Start by creating a Python file in the working directory.

touch api.py
Enter fullscreen mode Exit fullscreen mode

Great! Now import and initialize flask and flask restful

from flask import Flask
from flask_restful import Resource, Api, reqparse, abort, marshal, fields

# Initialize Flask
app = Flask(__name__)
api = Api(app)
Enter fullscreen mode Exit fullscreen mode

Be sure to import the required modules from flask-restful as shown above.

We'll be building a simple API that goes over the performing some basic CRUD operations on a datastore of books (or as it happens, in this case, a List of Dictionaries).

We're going to use a simple Python Dictionary for storage, But it can easily be swapped in with a real database solution, should you need to. But for the sake of simplicity, I've decided to go with a simple List of Dictionaries

Lets get started. First declare a List containing several Dictionaries, representing individual 'Book' elements

# A List of Dicts to store all of the books
books = [{
    "id": 1,
    "title": "Zero to One",
    "author": "Peter Thiel",
    "length": 195,
    "rating": 4.17
},
    {
    "id": 2,
    "title": "Atomic Habits ",
    "author": "James Clear",
    "length": 319,
    "rating": 4.35
}
]
Enter fullscreen mode Exit fullscreen mode

Next, Set up a Dictionary to determine the schema for the book object, as expected by the API. This will help with validating requests later.

# Schema For the Book Request JSON
bookFields = {
    "id": fields.Integer,
    "title": fields.String,
    "author": fields.String,
    "length": fields.Integer,
    "rating": fields.Float
}
Enter fullscreen mode Exit fullscreen mode

Flask Restful is an extension to the Flask Microframework that makes it a whole lot easier to build RESTful APIs

A fundamental building block provided by flask-restful is Resources. Resources give you easy access to multiple HTTP methods just by defining the methods on your Resource Class.

Here is the Book Resource class: This contains the HTTP routes for accessing, modifying and deleting each individual book entity. Now, Lets Break this down...

# Resource: Individual Book Routes
class Book(Resource):
    def __init__(self):
        # Initialize The Flsak Request Parser and add arguments as in an expected request
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument("title", type=str, location="json")
        self.reqparse.add_argument("author", type=str, location="json")
        self.reqparse.add_argument("length", type=int, location="json")
        self.reqparse.add_argument("rating", type=float, location="json")

        super(Book, self).__init__()

    # GET - Returns a single book object given a matching id
    def get(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        return{"book": marshal(book[0], bookFields)}

    # PUT - Given an id
    def put(self, id):
        book = [book for book in books if book['id'] == id]

        if len(book) == 0:
            abort(404)

        book = book[0]

        # Loop Through all the passed agruments
        args = self.reqparse.parse_args()
        for k, v in args.items():
            # Check if the passed value is not null
            if v is not None:
                # if not, set the element in the books dict with the 'k' object to the value provided in the request.
                book[k] = v

        return{"book": marshal(book, bookFields)}

        # Delete - Given an id
    def delete(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        books.remove(book[0])

        return 201
Enter fullscreen mode Exit fullscreen mode

First, In the init method of the class, you initialize the request parser. It'll allow you easy access to any variable on the flask.request and also validates the response based on the arguments provided

class Book(Resource):
    def __init__(self):
        # Initialize The Flsak Request Parser and add arguments as in an expected request
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument("title", type=str, location="json")
        self.reqparse.add_argument("author", type=str, location="json")
        self.reqparse.add_argument("length", type=int, location="json")
        self.reqparse.add_argument("rating", type=float, location="json")

        super(Book, self).__init__()
Enter fullscreen mode Exit fullscreen mode

Next, The GET Method, This one's really simple, it takes in an id and loops through the books list and checks each element's id with the specified ID, If a match is found, it returns that dict. The marshal method just makes sure the object that is being returned is being filtered through the fields defined in the bookFields dict.

# GET - Returns a single book object given a matching id
    def get(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        return{"book": marshal(book[0], bookFields)}
Enter fullscreen mode Exit fullscreen mode

The Put method is used to update the element with the specified id, It takes a response object with the fields to be updated First, It loops through the books list and checks each element's id with the specified ID, If a match is found, It parses all of the provided arguments using the reqparser.

Then, Loops through the parsed arguments and updates the fields as in the request object.

# PUT - Given an id
    def put(self, id):
        book = [book for book in books if book['id'] == id]

        if len(book) == 0:
            abort(404)

        book = book[0]

        # Loop Through all the passed agruments
        args = self.reqparse.parse_args()
        for k, v in args.items():
            # Check if the passed value is not null
            if v is not None:
                # if not, set the element in the books dict with the 'k' object to the value provided in the request.
                book[k] = v

        return{"book": marshal(book, bookFields)}
Enter fullscreen mode Exit fullscreen mode

The Delete method just simply takes in an id and deletes the element in the books list with the id.

# Delete - Given an id
    def delete(self, id):
        book = [book for book in books if book['id'] == id]

        if(len(book) == 0):
            abort(404)

        books.remove(book[0])

        return 201
Enter fullscreen mode Exit fullscreen mode

Next is the BookList class, This contains the routes dealing with operations on the entire database.

class BookList(Resource):
    def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument(
            "title", type=str, required=True, help="The title of the book must be provided", location="json")
        self.reqparse.add_argument(
            "author", type=str, required=True, help="The author of the book must be provided", location="json")
        self.reqparse.add_argument("length", type=int, required=True,
                                   help="The length of the book (in pages)", location="json")
        self.reqparse.add_argument(
            "rating", type=float, required=True, help="The rating must be provided", location="json")

    def get(self):
        return{"books": [marshal(book, bookFields) for book in books]}

    def post(self):
        args = self.reqparse.parse_args()
        book = {
            "id": books[-1]['id'] + 1 if len(books) > 0 else 1,
            "title": args["title"],
            "author": args["author"],
            "length": args["length"],
            "rating": args["rating"]
        }

        books.append(book)
        return{"book": marshal(book, bookFields)}, 201
Enter fullscreen mode Exit fullscreen mode

The Init method initializes the request parser. It parses the request JSON Object and also validates it based on the arguments provided.

def __init__(self):
        self.reqparse = reqparse.RequestParser()
        self.reqparse.add_argument(
            "title", type=str, required=True, help="The title of the book must be provided", location="json")
        self.reqparse.add_argument(
            "author", type=str, required=True, help="The author of the book must be provided", location="json")
        self.reqparse.add_argument("length", type=int, required=True,
                                   help="The length of the book (in pages)", location="json")
        self.reqparse.add_argument(
            "rating", type=float, required=True, help="The rating must be provided", location="json")
Enter fullscreen mode Exit fullscreen mode

The Get method simply returns all the elements in the books list

def get(self):
        return{"books": [marshal(book, bookFields) for book in books]}
Enter fullscreen mode Exit fullscreen mode

The Post method takes a JSON Object. Parses it , creates a new dict and appends it to the Books list.

def post(self):
        args = self.reqparse.parse_args()
        book = {
            "id": books[-1]['id'] + 1 if len(books) > 0 else 1,
            "title": args["title"],
            "author": args["author"],
            "length": args["length"],
            "rating": args["rating"]
        }

        books.append(book)
        return{"book": marshal(book, bookFields)}, 201
Enter fullscreen mode Exit fullscreen mode

And Thats it! Now just attach those Resource classes to some endpoints and test it out!

api.add_resource(BookList, "/books")
api.add_resource(Book, "/books/<int:id>")

if __name__ == "__main__":
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

Deploy on Docker:

Alright, Now lets deploy this microservice API to a docker container.

First, create a Dockerfile in the project directory.

A Docker file is in essence a set of instructions for building an Image which is a blueprint which your container will run off of.

Lets go over this line by line.

FROM python:3.8

RUN pip3 install pipenv

ENV PROJECT_DIR /usr/src/flaskbookapi

WORKDIR ${PROJECT_DIR}

COPY Pipfile .
COPY Pipfile.lock .
COPY . .

RUN pipenv install --deploy --ignore-pipfile

EXPOSE 5000

CMD ["pipenv", "run", "python", "api.py"]
Enter fullscreen mode Exit fullscreen mode

This use the python 3.8 image from Docker Hub as our base image, This makes sure we have Python and all of its dependencies on the container

FROM python:3.8
Enter fullscreen mode Exit fullscreen mode

This installs pipenv on the container.

RUN pip3 install pipenv
Enter fullscreen mode Exit fullscreen mode

The first line sets a environment variable as a path to a directory to store the projects code.

Then the next command sets that directory as the working directory.

ENV PROJECT_DIR /usr/src/flaskbookapi

WORKDIR ${PROJECT_DIR}
Enter fullscreen mode Exit fullscreen mode

Next, We copy over all of our files to the container

COPY Pipfile .
COPY Pipfile.lock .
COPY . .
Enter fullscreen mode Exit fullscreen mode

This command runs pipenv install with the —deploy flag and also sets it to ignore the pipfile and just use the pipfile.lock to install dependencies

RUN pipenv install --deploy --ignore-pipfile
Enter fullscreen mode Exit fullscreen mode

Next, We expose the 5000 port to be able to use it.

EXPOSE 5000
Enter fullscreen mode Exit fullscreen mode

Finally, we run our program using the CMD command.

CMD ["pipenv", "run", "python", "api.py"]
Enter fullscreen mode Exit fullscreen mode

Now lets use that Dockerfile to build an image which will later be used to make a container

docker build -t flaskbookapi:1.0 .
Enter fullscreen mode Exit fullscreen mode

Now, finally! Fire up a container with the image we just built!

docker run -p 5000:5000 --name FlaskBookAPI flaskbookapi:1.0
Enter fullscreen mode Exit fullscreen mode

And Boom! Out API is up and running! On a Docker container

$ docker run -p 5000:5000 --name FlaskBookAPI flaskbookapi:1.0
 * Serving Flask app "api" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
Enter fullscreen mode Exit fullscreen mode

Download the Project from GitHub:

https://github.com/SwarnimWalavalkar/rest-api-microservice-docker

Further Reading:

Flask Documentation: https://flask.palletsprojects.com/en/1.1.x/

Flast-Restful Documentation: https://flask-restful.readthedocs.io/en/latest/

Docker Documentation: https://docs.docker.com/

Pipenv: https://pipenv-fork.readthedocs.io/en/latest/

Latest comments (0)