Looking for a specific post? Checkout the Blog Index.

By Kimserey Lam with

Python Web Api With Flask

Jan 18th, 2019 - written by Kimserey with .

Flask is a microframework for python providing building blocks to compose websites and web API quickly. It has a vast ecosystem driven by open source libraries maintained and used by many developers. Today we will see how we can setup a simple todo web API using Flask and how we can setup OpenAPI 3.0 (OAS3 - previously known as Swagger).

Swagger UI

Flask

Flask is a microframework for python providing building blocks to compose websites and web API quickly. In this tutorial we will be creating a simple TODO allowing to perform the following actions:

  • List todos
  • Get a todo by name
  • Post a new todo

We start by creating a folder and navigating to that folder:

1
2
mkdir todo_flask
cd todo_flask

We then setup a virtual environment and install Flask.

1
2
py -m venv env
pip install flask

Next we define our common file structure:

1
2
3
4
5
6
/todo_flask
  /env
  /todo_flask
    __init__.py
    __main__.py
    todo_flask.py

In __init__.py, we define the import for package import:

1
2
3
from .todo_flask import *

__version__ = '0.0.1'

In todo_flask.py, we define a test endpoint listing todos:

1
2
3
4
5
6
7
8
9
10
from flask import Flask, jsonify

app = Flask(__name__)

@app.route("/todos")
def get_todos():
  return jsonify([{ 
    'name': 'Do groceries', 
    'task': 'Go to Tesco, Buy apples, lemons, yogurts.' 
  }])

Lastly in __main__.py, we run the Flask application:

1
2
3
4
from todo_flask import app

if __name__ == "__main__":
    app.run()

We now have a simple single endpoint API returning a list of todos. We can test that our server is running properly with:

1
py -m todo_flask

And when we hit http://localhost:5000/todos, we should be able to see our todos.

1
[{"name":"Do groceries","task":"Go to Tesco, Buy apples, lemons, yogurts."}]

We can now define the rest of our enpoints:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from flask import Flask, jsonify, request

app = Flask(__name__)

todos = {}

@app.route("/todos")
def get_todos():
  return jsonify(list(todos.values())), 200

@app.route("/todos", methods=['POST'])
def post_todo():
  json_data = request.get_json()
  if not json_data:
    return jsonify({'message': 'No input data provided'}), 400
  else:
      # process the body
      return '', 200

@app.route("/todos/<string:name>")
def get_todo(name):
  if name in todos:
    return jsonify(todos[name]), 200
  else:
    return jsonify({ 'error': 'Todo not found.' }), 400

We specify the method on the app.route decorator with methods=['POST']. And also specify a converter which validate the types of the parameter with <string:name>. This converter is part of the default converter from werkzeug.routing, the default converter are <string|uuid|int|float|any(x,y,z):param>. For parsing the body of the request, we use request.get_json() from request.

Flasgger

So far we have a simple web API composed by three endpoints which we can test using curl or a UI like postman. Another common way to test APIs is to provide an OpenAPI definition - previously known as Swagger definition. To achieve that we will be installing Flasgger.

1
2
pip install -U setuptools
pip install flasgger

We then instantiate it by importing Swagger from flasgger in todo_flask.py:

1
2
3
4
5
6
7
from flask import Flask, jsonify, request
from flasgger import Swagger

app = Flask(__name__)
swagger = Swagger(app)

## Rest of the application

And when we hit http://localhost:5000/apidocs (the default location for Swagger UI), we should now see that Swagger is running properly and is looking at http://localhost:5000/apispec_1.json for the json specification.

Next we can see how we implement the definition of the following:

Response definition

Next we can add the OAS 3.0 definition for each endpoint starting from get_todo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@app.route("/todos")
def get_todos():
  """
  Get all todos
  ---
  description: Get all todos.
  tags:
    - todos
  responses:
    200:
      description: List of all todos.
      content:
        application/json:
          schema:
            type: array
            items:
              $ref: '#/components/schemas/Todo'
            
  """
  return jsonify(list(todos.values())), 200

We start first by a description description: Get all todos.. Followed by tags which define a grouping for the operation, this grouping will then be reflected on the Swagger UI.

1
2
tags:
  - todos

Lastly we specify the responses:

1
2
3
4
5
6
7
8
9
responses:
  200:
    description: List of all todos.
    content:
      application/json:
        schema:
          type: array
          items:
            $ref: '#/components/schemas/Todo'

The responses are defined by the status codes. It then contains the description and content which itself with the response type and the schema under that response type. For example here we return application/json, and for the schema, we specify an array and reference to a Todo schema.

Schema Type definition

We then define the Todo schema in the template of Swagger:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
app = Flask(__name__)
swagger = Swagger(app,
  template= {
    "swagger": "3.0",
    "openapi": "3.0.0",
    "info": {
        "title": "TODO",
        "version": "0.0.1",
    },
    "components": {
      "schemas": {
        "Todo": {
          "properties": {
            "name": {
              "type": "string"
            },
            "task": {
              "type": "string"
            }
          }
        }
      }
    }
  }
)

Request Body definition

This will allow us to reuse it in the POST:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@app.route("/todos", methods=['POST'])
def post_todo():
  """
  Create a new todo
  ---
  description: Create a new todo.
  tags:
    - todos
  requestBody:
    description: The todo to create.
    required: true
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Todo'
  responses:
    200:
      description: Empty.
  """
  data = request.get_json()
  if not data:
    return jsonify({'message': 'No input data provided'}), 400
  else:
    todos[data['name']] = data
    return '', 200

We specify the body of the request using requestBody where we specify the content with the content type and the schema which refers to the Todo schema we used in GET.

1
2
3
4
5
6
7
requestBody:
  description: The todo to create.
  required: true
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/Todo'

Path Parameter defintion

Lastly we can add the definition for the GET /todos/{name} endpoint:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@app.route("/todos/<string:name>")
def get_todo(name):
  """
  Get the todo with the name provided.
  ---
  description: >
    Get the todo with the name provided by 
    getting it using the **name** parameter provided!
  tags:
    - todos
  parameters:
    - name: name
      in: path
      description: Name of the todo
      required: true
      schema:
        type: string
  responses:
    200:
      description: A todo.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Todo'
  """
  if name in todos:
    return jsonify(todos[name]), 200
  else:
    return jsonify({ 'error': 'Todo not found.' }), 400

We specify the paramter definition with parameters array where we make sure to specify that the parameters comes from in: path:

1
2
3
4
5
6
7
parameters:
  - name: name
    in: path
    description: Name of the todo
    required: true
    schema:
      type: string

Putting it all together

And here is the complete todo_flask.py code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
from flask import Flask, jsonify, request
from flasgger import Swagger

app = Flask(__name__)
swagger = Swagger(app,
  template= {
    "swagger": "3.0",
    "openapi": "3.0.0",
    "info": {
        "title": "TODO",
        "version": "0.0.1",
    },
    "components": {
      "schemas": {
        "Todo": {
          "properties": {
            "name": {
              "type": "string"
            },
            "task": {
              "type": "string"
            }
          }
        }
      }
    }
  }
)

todos = {}

@app.route("/todos")
def get_todos():
  """
  Get all todos
  ---
  description: Get all todos.
  tags:
    - todos
  responses:
    200:
      description: List of all todos.
      content:
        application/json:
          schema:
            type: array
            items:
              $ref: '#/components/schemas/Todo'
            
  """
  return jsonify(list(todos.values())), 200

@app.route("/todos", methods=['POST'])
def post_todo():
  """
  Create a new todo
  ---
  description: Create a new todo.
  tags:
    - todos
  requestBody:
    description: The todo to create.
    required: true
    content:
      application/json:
        schema:
          $ref: '#/components/schemas/Todo'
  responses:
    200:
      description: Empty.
  """
  data = request.get_json()
  if not data:
    return jsonify({'message': 'No input data provided'}), 400
  else:
    todos[data['name']] = data
    return '', 200

@app.route("/todos/<string:name>")
def get_todo(name):
  """
  Get the todo with the name provided.
  ---
  description: >
    Get the todo with the name provided by 
    getting it using the **name** parameter provided!
  tags:
    - todos
  parameters:
    - name: name
      in: path
      description: Name of the todo
      required: true
      schema:
        type: string
  responses:
    200:
      description: A todo.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Todo'
  """
  if name in todos:
    return jsonify(todos[name]), 200
  else:
    return jsonify({ 'error': 'Todo not found.' }), 400

We then now have a complete OAS3.0 definition and the UI should now display as followed:

Swagger UI

Conclusion

Today we explored Flask, a microframework for python used to build websites and web APIs. We started by building a simple TODO list API with POST and GET endpoints taking parameters in URL and request body. We then moved on to add OpenAPI definition, previously known as Swagger definition, and defining all the elements to allow the Swagger UI to be used to test our API. Hope you liked this post, see you on the next one!

Designed, built and maintained by Kimserey Lam.