A template for beautiful REST APIs using Flask, Swagger UI, Streamlit, Docker and Gunicorn.

Ilias Mansouri
7 min readOct 23, 2021

--

Introduction

The goal of this article is to share with you one of my templates to rapidly create a REST API. This template was made in a Machine Learning context addressing the following criteria:

  • Rapid app/microservices development and delivery.
  • Separate logic for front/backend.
  • Frontend should be simple to easily interface with ML/DL based objects.
  • Backend should have a clear structure in terms of routing, error handling and core functions.
  • Simple overview of I/O, descriptions, parameters concerning the endpoint(s).
  • Clear API contract should allow early testing of API’s to allow a frontend team to easily interact with the endpoints while the ML Engineers are doing their thing.

Included in this template, we’ll discuss the following:

  • The commonly used Flask micro framework.
  • Swagger UI which automagically generate documentation allowing to visualize and interact with the API’s resources without having any of the implementation logic in place.
  • Docker which will allows us to containerize our application.
  • Streamlit is employed as a frontend to easily test our POST Requests on the endpoints.
  • Gunicorn, a WSGI HTTP server to run our Python web application.

Getting started

The code for this article can be found here and make sure to follow the installation instructions below if you’d like to follow along. Make sure to have Anaconda and a Python 3.6 environment:

pip install -r front_end/requirements.txtdocker build -t “swagger-ui” -f back_end/Dockerfile .docker run -p 8296:8296 swagger-uistreamlit run main.py

We first install the necessary packages for our Streamlit dashboard, then we build and run our API endpoint which can finally be accessed by our dashboard. If all went well you should have a similar view like below:

Left: our backend API ready to get a POST Request. Right: our dashboard successfully started.

Going to the Local URL, you should see this:

What happened is that our Streamlit client/dashboard requested data from the server, using a GET method, and got the json as seen above. The other method that we’ll discuss shortly is the POST which is used to send data to a server to create/update a resource. If you select another option from the radio button: corresponding media type (image, audio or text) will be send to the server/backend which processes this and sends a result. Again, in the context of ML projects it’s interesting to see how we can process different media type.

Let’s dive deeper and explain how the components interact starting with the Streamlit dashboard. We will discuss how the routing and POST requests can be send to the backend.

Streamlit Dashboard

Streamlit is an open-source Python library allowing us to easily create and deploy custom web apps for machine learning and data science. Furthermore, it is very easy to create interactive dashboards in a pythonic/intuitive way. Finally, it is out of the box compatible with libraries like Keras, TF, PyTorch, Scikit-learn, Altair, Bokeh, etc. We will not go further into Streamlit but below we find the code to run the dashboard:

front_end/app.py

The PostRequest class is responsible for sending and handling the request responses. From TxType, an enumeration of allow mediatypes, we generate the radiobuttons at the sidebar and wait for the radio value. This value defaults to the first element in TxType (“Test”) and on change from user input will accordingly be set to the new value.

This is then passed to the PostRequest’s post-method:

front_end/post.py

depending on this radio button value, the corresponding __post() function called. Each of these shows how to handle corresponding media-type. Image and audio are typically base64-encoded before put inside a dictionary, text can be immediately passed as a string. If you need to only send data with no other parameters, you could send the media as formdata with no required base64 encoding necessary.

After media-specific processing, each post() follows the following:

where a post request is send to the backend’s url and media-specific endpoint. StreamLitResponse will handle the response to be shown, depending on the media-type, on our dashboard by returning the appropriate Streamlit widget which is invoked in front_end/app.py line:10.

In the PostRequest class: lines 32, 40, 53 and 61 we see that the individual endpoints are:

  • /greeting
  • /process_img
  • /process_audio
  • /process_text

Now that we’ve seen how we can send either POST/GET requests to a server and expect a result, it’s time to dig into the server side. First, we’ll discuss Swagger which provides a useful API contract and interface. We’ll specifically discuss how we can define the 4 aforementioned endpoints.

Swagger

Swagger UI, which has evolved into the OpenAPI specification, allows you to describe, visualize and generate documentation or even boilerplate code for your RESTful web services. Following an API/documentation first approach to building a RESTful API in Python, let’s dive into the yaml-specification found in back_end/configs/swagger.yaml:

We start with some metadata. Additionally, host-url, basepaths, scheme, version and supported MIME types can be added. Then follows a section where we define the individual endpoints in our API and the corresponding supported HTTP methods. Each endpoint has an operationId which denotes the function name in our Python code followed by specifications of the parameters. The schema of the payloads/responses is defined below definitions:

For both payload and response used type is object. As such, we need to define the property names and specify a schema for each of them. Not only is it possible to specify which properties are required but also if they should be read/write only. Finally, it’s also possible to have nested objects.

You can find additional documentation here and experiment with configuration files here. This config file will already generate the following:

No extra documentation needed!

Understanding how we can define the different routes of an endpoint for an API in a configuration file allows us to generate the necessary documentation and also test the endpoints. Once the routing configuration done, it’s time for us to actually implement those.

Python

We’ll discuss how we can use Zalando’s Connexion library which handles HTTP requests based on our configuration file. It acts as a simple wrapper around Flask reducing the amount of boilerplate code.

Our app is created in the __init__.py found at back_end/app/flask:

We create our app (line 11) by passing the directory containing our swagger.conf and also a swagger_ui_path. The swagger_ui_bundle contains the static files for swagger-ui as a python package and allows to easily serve the UI. We add the CORS policy, add our API specification file and add error handlers if necessary.

We then map our endpoints to functions at back_end/app/flask/routes.py:

In this case, functions were created with the corresponding endpoint name. This is not necessary. As aforementioned, it’s in the swagger.yaml that we define the function name for each endpoint with the operationId.

The actual functions and processing of the requests happens in core.py making for a clean separation and overview of the function calls and their respective endpoint routes.

In the case of more complex applications, in core.py would be invocations to class specific methods defined elsewhere where common logic would reside in the utils folder and model logic and/or weights would be inside the model folder.

We’re almost done! The code to process different media types from different endpoints and to return the corresponding results is finished. We now only need to package it into a nice re-usable container.

Docker Containerisation

Starting from a Python 3.6 base, we install the necessary packages. We copy the local backend content into the image’s backend followed by a pip install. The files in the common folder which is used by both frontend and backend is copied into back_end/app/common.

Although we are using Flask, it’s not suitable for production and needs to be put behind a real web server able to communicate with Flask through a WSGI protocol. This is where Gunicorn comes into play.

Demo

Run the follow commands:

conda create -n swagger python=3.6
pip install -r front_end/requirements.txt
docker build -t “swagger-ui” -f back_end/Dockerfile .
docker run -d -p 8296:8296 swagger-ui
streamlit run main.py
You’ll be greeted with such a window:

That’s pretty much it! Again, this is just a template for a typical model serving application. Project structure would be really dependent on the complexity and size of your code. Furthermore, FastAPI could be preferred over Flask. Instead of sending/receiving base64 encoded data, dataforms are also a possibility with the added convenience that testing from the swagger ui is more easier as it doesn’t require manual base64 encoding of the data.

Again, the code is here. Have fun!

--

--