Python: Flask-RESTX and the Swagger UI automatic documentation.
Flask-RESTX provides support for building REST APIs. The resulting APIs come automatically with the Swagger UI page as a documentation page and a UI for API testing.
Python: Flask-RESTX and the Swagger UI automatic documentation. |
Updated 25/July/2024:
With the FastAPI
web framework, we get the Swagger UI
by default. I find FastAPI
to be very impressive. I’ve been writing
about it in the FastAPI Learning Series.
Updated 14/July/2022: Final code for this post can be cloned from GitHub using:
git clone -b v1.0.0 https://github.com/behai-nguyen/flask-restx-demo.git
I've recently checked out Flask-RESTX, and along with it OpenAPI Specification, and Swagger UI, which:
– renders OpenAPI specs as interactive API documentation.
See What Is OpenAPI?
Flask-RESTX provides support for building REST APIs. The resulting APIs come automatically with the Swagger UI page as documentation and a UI for API testing.
Please note, I'm using the term “REST API” out of conformity. It's a complicated topic, and there are a lot of discussions on the net, please see among others:
- https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
- https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
- https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
- https://phauer.com/2015/restful-api-design-best-practices/
- https://stackoverflow.com/questions/2001773/understanding-rest-verbs-error-codes-and-authentication?noredirect=1&lq=1
- https://stackoverflow.com/questions/7140074/restfully-design-login-or-register-resources
- https://stackoverflow.com/questions/15098392/which-http-method-should-login-and-logout-actions-use-in-a-restful-setup?noredirect=1&lq=1
- https://stackoverflow.com/questions/51376453/how-to-design-a-restful-url-for-login?noredirect=1&lq=1
- https://stackoverflow.com/questions/6068113/do-sessions-really-violate-restfulness
- https://stackoverflow.com/questions/319530/restful-authentication?rq=1
- Google search “rfc rest api”
I'm keen on seeing how this works. My focus was primarily getting out some inbuilt Swagger UI page. I want to understand how it works before focusing on other aspects of Flask-RESTX.
I fancy a website whereby I can keep the information on trees. To start off, I would just have two APIs: one to create a new tree record in the database, the other just returns all records in the table! This is just to keep things simple, nobody in their right mind would return all records in a table in a one go!
The screen capture below shows the Swagger UI page for the two tree APIs:
POST API create a tree is shown in detail in the following screen capture. This API has three ( 3 ) mandatory form fields, two ( 2 ) are normal string, one ( 1 ) is a URL. And four ( 4 ) possible response codes. Click on “Try it out” button on the top right hand side corner to try this method. To simulate 400 Validation error, enter some digits into either name field.
The final API method has no input parameter, and also a few possible response codes and messages.
Please note, the Swagger UI page just happens automatically, we don't have to download and install anything.
As we shall see, there're steps we have to follow to get the Swagger UI page correctly, but those steps are also part of the API code... so I do feel we get it for almost free.
To start off, we create the API boilerplate code. The initial files and directory structure is as follows:
<pre>
f:\flask_restx_demo
|
|-- .env
|-- app.py
|-- setup.py
|
|-- src\
| |
| |-- flask_restx_demo\
| |
| |-- __init__.py
| |-- config.py
Let's list the content of each file, since they are very short.
File f:\flask_restx_demo\.env
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=">s3g;?uV^K=`!(3.#ms_cdfy<c4ty%"
File f:\flask_restx_demo\app.py
1
2
3
4
5
"""Flask Application entry point."""
from flask_restx_demo import create_app
app = create_app()
File f:\flask_restx_demo\setup.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"""Installation script for flask_restx demo project."""
from pathlib import Path
from setuptools import setup, find_packages
setup(
name='flask-restx-demo',
description='Flask-RESTX and Swagger UI Demo.',
version='1.0.0',
author='Van Be Hai Nguyen',
author_email='behai_nguyen@hotmail.com',
packages=find_packages(where="src"),
package_dir={"": "src"},
python_requires='>=3.10',
install_requires=[
'Flask',
'python-dotenv',
'Flask-RESTX',
'Flask-SQLAlchemy',
],
)
File f:\flask_restx_demo\src\flask_restx_demo\__init__.py
1
2
3
4
5
6
7
8
9
10
11
"""Flask app initialization via factory pattern."""
from flask import Flask
from flask_restx_demo.config import get_config
def create_app():
app = Flask( 'flask-restx-demo' )
app.config.from_object( get_config() )
return app
File f:\flask_restx_demo\src\flask_restx_demo\config.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
"""Flask app config settings."""
import os
class Config:
"""Set Flask configuration from .env file."""
# General Config
SECRET_KEY = os.getenv( 'SECRET_KEY' )
FLASK_APP = os.getenv( 'FLASK_APP' )
FLASK_ENV = os.getenv( 'FLASK_ENV' )
def get_config():
"""Retrieve environment configuration settings."""
return Config
Run the following commands to create a virtual environment venv and activate it:
F:\flask_restx_demo>C:\PF\Python310\python.exe -m pip install --upgrade pip
F:\flask_restx_demo>C:\PF\Python310\python.exe -m pip install --user virtualenv
F:\flask_restx_demo>C:\Users\behai\AppData\Roaming\Python\Python310\Scripts\virtualenv.exe venv
F:\flask_restx_demo>venv\Scripts\activate.bat
Run the below command to editable install required packages:
(venv) F:\flask_restx_demo>venv\Scripts\pip.exe install -e .
Command:
(venv) F:\flask_restx_demo>venv\Scripts\flask.exe routes
would show the following:
Endpoint Methods Rule
-------- ------- -----------------------
static GET /static/<path:filename>
Next, we'll need to configure flask's Blueprint and flask_restx's Api. Our directory structure should now look like below, where ★ indicates new files, and ☆ indicates files to be modified:
f:\flask_restx_demo
|
|-- .env
|-- app.py
|-- setup.py
|
|-- src\
| |
| |-- flask_restx_demo\
| |
| |-- __init__.py ☆
| |-- config.py
| |-- api\
| |
| |-- __init__.py ★
|
|-- venv\
New file F:\flask_restx_demo\src\flask_restx_demo\api\__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
""" Flask-RESTX API blueprint configuration. """
from flask import Blueprint
from flask_restx import Api
api_bp = Blueprint( 'api', __name__, url_prefix='/api/v1' )
api = Api(
api_bp,
version='1.0',
title='Flask-RESTX API Demo',
description='Welcome to Flask-RESTX API with Swagger UI documentation',
doc='/ui',
)
Following documentations, I'm assigning version 1.0 to the first implementation, also the API path starts with /api/v1. And /api/v1/ui is the Swagger UI path.
Modified file f:\flask_restx_demo\src\flask_restx_demo\__init__.py
Please note the lines added are line 11 and line 13:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"""Flask app initialization via factory pattern."""
from flask import Flask
from flask_restx_demo.config import get_config
def create_app():
app = Flask( 'flask-restx-demo' )
app.config.from_object( get_config() )
from flask_restx_demo.api import api_bp
app.register_blueprint( api_bp )
return app
If everything goes well, and it should go well, the command:
(venv) F:\flask_restx_demo>venv\Scripts\flask.exe routes
now would show the following:
Endpoint Methods Rule
---------------- ------- --------------------------
api.doc GET /api/v1/ui
api.root GET /api/v1/
api.specs GET /api/v1/swagger.json
restx_doc.static GET /swaggerui/<path:filename>
static GET /static/<path:filename>
The skeleton for the APIs we're going to write is pretty much in place. We shall also need a database, and a table to store trees' information. Let's do the database first.
We're using SQLAlchemy with a SQLite database. The flask_restx_demo.db database file will be automatically created under the project directory when we first run the application.
File f:\flask_restx_demo\.env -- final version
Added line 4 and line 5 which define database info:
FLASK_APP=app.py
FLASK_ENV=development
SECRET_KEY=">s3g;?uV^K=`!(3.#ms_cdfy<c4ty%"
SQLALCHEMY_DATABASE_URI="sqlite:///flask_restx_demo.db"
SQLALCHEMY_TRACK_MODIFICATIONS=True
Now, we'll need to update config.py to read the two new pieces of database information. The updated content follows below. New codes added are lines line 11 and line 12:
File f:\flask_restx_demo\src\flask_restx_demo\config.py -- final version
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"""Flask app config settings."""
import os
class Config:
"""Set Flask configuration from .env file."""
# General Config
SECRET_KEY = os.getenv( 'SECRET_KEY' )
FLASK_APP = os.getenv( 'FLASK_APP' )
FLASK_ENV = os.getenv( 'FLASK_ENV' )
SQLALCHEMY_DATABASE_URI = os.getenv( 'SQLALCHEMY_DATABASE_URI' )
SQLALCHEMY_TRACK_MODIFICATIONS = os.getenv( 'SQLALCHEMY_TRACK_MODIFICATIONS' )
def get_config():
"""Retrieve environment configuration settings."""
return Config
The application factory must also be updated to manage database extension object.
File f:\flask_restx_demo\src\flask_restx_demo\__init__.py -- final version
Please note the lines added are line 3, line 7 and lines 18-20. It's pretty much run-of-the-mill Python code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"""Flask app initialization via factory pattern."""
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_restx_demo.config import get_config
db = SQLAlchemy()
def create_app():
app = Flask( 'flask-restx-demo' )
app.config.from_object( get_config() )
from flask_restx_demo.api import api_bp
app.register_blueprint( api_bp )
db.init_app( app )
with app.app_context():
db.create_all()
return app
The database codes are out of the way. Next comes the codes for the APIs. We'll need several new files as indicated by ★ in the new directory layout below:
f:\flask_restx_demo
|
|-- .env
|-- app.py
|-- setup.py
|
|-- src\
| |
| |-- flask_restx_demo\
| |
| |-- __init__.py
| |-- config.py
| |
| |-- api\
| | |
| | |-- __init__.py ☆
| | |
| | |-- trees\
| | |
| | |-- bro.py ★
| | |-- dto.py ★
| | |-- routes.py ★
| | |-- __init__.py ★
| |
| |-- models\
| |
| |-- tree.py ★
|
|-- venv\
File F:\flask_restx_demo\src\flask_restx_demo\models\tree.py
Tree class is a SQLAlchemy class, which basically represents a table in a database, in this case the table name is “tree”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"""Class definition for Tree model."""
from flask_restx_demo import db
class Tree( db.Model ):
"""Tree model for a generic resource for Flask-RESTX API Demo."""
__tablename__ = "tree"
id = db.Column( db.Integer, primary_key=True, autoincrement=True )
scientific_name = db.Column( db.String(128), unique=True, nullable=False )
common_name = db.Column( db.String(128), nullable=False )
wiki_url = db.Column( db.String(255), nullable=False )
def __repr__(self):
return f"<Tree scientific name={self.scientific_name}, common name={self.common_name}>"
@classmethod
def find_by_scientific_name( cls, scientific_name ):
return cls.query.filter_by( scientific_name=scientific_name ).first()
Under directory
F:\flask_restx_demo\src\flask_restx_demo\api\trees
__init__.py is to indicate a regular package. It's empty.
DTO is short for Data Transfer Object -- please see Google search: “swagger ui data transfer object”. Basically DTO defines API model classes to serialise database model classes to JSON objects before sending them back as HTTP responses. See Response marshalling.
File F:\flask_restx_demo\src\flask_restx_demo\api\trees\dto.py
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
"""Parsers and serializers for /trees API endpoints."""
import re
from flask_restx import Model
from flask_restx.fields import String
from flask_restx.inputs import URL
from flask_restx.reqparse import RequestParser
def tree_name( name ):
"""Validation method for a string containing only letters, '-' and space."""
if not re.compile(r"^[A-Za-z, ' ', -]+$").match(name):
raise ValueError(
f"'{name}' contains one or more invalid characters. Tree name must "
"contain only letters, hyphen and space characters."
)
return name
create_tree_reqparser = RequestParser( bundle_errors=True )
create_tree_reqparser.add_argument(
'scientific_name',
type=tree_name,
location='form',
required=True,
nullable=False,
case_sensitive=True,
)
create_tree_reqparser.add_argument(
'common_name',
type=tree_name,
location='form',
required=True,
nullable=False,
case_sensitive=True,
)
create_tree_reqparser.add_argument(
'wiki_url',
type=URL( schemes=[ 'http', 'https' ] ),
location='form',
required=True,
nullable=False,
)
update_tree_reqparser = create_tree_reqparser.copy()
update_tree_reqparser.remove_argument( 'scientific_name' )
tree_model = Model( 'Tree', {
'scientific_name': String,
'common_name': String,
'wiki_url': String,
})
Flask-RESTX uses RequestParser to manage incoming request info. Line 18, we instantiate an instance of RequestParser to manage creating new trees requests:
create_tree_reqparser = RequestParser( bundle_errors=True )
Please note, bundle_errors=True is explained very clearly in RequestParser | section Error Handling, basically, setting bundle_errors=True to collect and to return all errors at once.
A tree requires three ( 3 ) pieces of information. Lines 19-41, we define the properties for each of the create_tree_reqparser's arguments. As previously mentioned, they're mandatory form fields, that would explain location='form', required=True and nullable=False. type indicates how an argument ( field ) should be validated. For scientific_name and common_name, the validation is based on the custom method:
def tree_name( name ):
Whereas for type, it must a valid URL.
Lines 43-44 are known as “parser inheritance”:
update_tree_reqparser = create_tree_reqparser.copy()
update_tree_reqparser.remove_argument( 'scientific_name' )
We're not implementing an update API. This is just to show how inheritance works. update_tree_reqparser is a “clone” of create_tree_reqparser but without the scientific_name argument.
Finally, lines 46-50 defines the serialised API model class:
tree_model = Model( 'Tree', {
'scientific_name': String,
'common_name': String,
'wiki_url': String,
})
BRO or Business Rule Object is a convention used by a company I worked for earlier in my career, during the COM/COM+/DCOM days... Since there's DTO, so I thought it would be nice to revive this naming once again.
File F:\flask_restx_demo\src\flask_restx_demo\api\trees\bro.py
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
"""Business rules ( logic ) for /trees API endpoints."""
from http import HTTPStatus
from flask import jsonify
from flask_restx import abort, marshal
from flask_restx_demo import db
from flask_restx_demo.models.tree import Tree
from flask_restx_demo.api.trees.dto import tree_model
def _create_successful_response( status_code, message ):
response = jsonify(
status="success",
message=message,
)
response.status_code = status_code
response.headers[ 'Cache-Control' ] = 'no-store'
response.headers[ 'Pragma' ] = 'no-cache'
return response
def create_tree( tree_dict ):
if Tree.find_by_scientific_name( tree_dict['scientific_name'] ):
abort( HTTPStatus.CONFLICT, f"{tree_dict['scientific_name']} is already entered", status="fail" )
new_tree = Tree( **tree_dict )
db.session.add( new_tree )
db.session.commit()
return _create_successful_response(
status_code=HTTPStatus.CREATED,
message='successfully created',
)
def retrieve_tree_list():
data = Tree.query.all()
response_data = marshal( data, tree_model )
response = jsonify( response_data )
return response
Line 21 defines an API method:
def create_tree( tree_dict ):
This method first ensures that the new scientific_name is not already in the database, then via the ** operator, it unpacks the dictionary argument into keyword arguments, and creates a new record for the new tree. See also The Python Dictionary ** unpack operator.
Once done, it sends back a JSON response consisting of a code and a message.
Line 32 defines another API method:
def retrieve_tree_list():
This method first retrieves data from the database table, then serialises database data into the API model class defined in module dto.py earlier. And finally returns this serialised data as JSON via HTTP response.
The final new module implements API endpoints:
File F:\flask_restx_demo\src\flask_restx_demo\api\trees\routes.py
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
"""API endpoint definitions for /trees namespace."""
from http import HTTPStatus
from flask_restx import Namespace, Resource
from flask_restx_demo.api.trees.dto import (
create_tree_reqparser,
tree_model,
)
from flask_restx_demo.api.trees.bro import (
create_tree,
retrieve_tree_list,
)
tree_ns = Namespace( name="trees", validate=True )
# tree_ns.models[ tree_model.name ] = tree_model
# tree_ns.add_model( tree_model.name, tree_model )
tree_ns.model( tree_model.name, tree_model )
@tree_ns.route( "", endpoint='tree_list' )
@tree_ns.response( int(HTTPStatus.BAD_REQUEST), 'Validation error.' )
@tree_ns.response( int(HTTPStatus.INTERNAL_SERVER_ERROR), 'Internal server error.' )
class TreeList( Resource ):
""" Handles HTTP requests to URL: /trees. """
@tree_ns.response( int(HTTPStatus.OK), 'Retrieved tree list.' )
def get( self ):
""" Retrieve tree list. """
return retrieve_tree_list()
@tree_ns.response(int(HTTPStatus.CREATED), 'Added new tree.' )
@tree_ns.response(int(HTTPStatus.CONFLICT), 'Tree name already exists.' )
@tree_ns.expect( create_tree_reqparser )
def post( self ):
""" Create a tree. """
tree_dict = create_tree_reqparser.parse_args()
return create_tree( tree_dict )
For API, class flask_restx.Namespace( ... ) is what flask.Blueprint is to flask.Flask.
Lines 16-19 we instantiate an instance of flask_restx.Namespace, and register our target DTO API model class with it: it seems we can do this via three ( 3 ) different methods, we're using the documented method ( line 19 ); commented lines 17, 18 are also valid codes.
In Flask-RESTX context, trees are class flask_restx.Resource( ... ), and so class TreeList extends from this abstract class as per documentation. This class has several decorators. The first one:
@tree_ns.route( "", endpoint='tree_list' )
The value of the first parameter is blank, which indicates that this resource doesn't have an endpoint of its own, it'll use the endpoint provided by the Namespace. That means, both API methods are accessible via:
/api/v1/trees
For example:
http://127.0.0.1:5000/api/v1/trees
The requested HTTP method, GET or POST differentiates between the two.
The next two decorators declare response codes and response messages:
@tree_ns.response( int(HTTPStatus.BAD_REQUEST), 'Validation error.' )
@tree_ns.response( int(HTTPStatus.INTERNAL_SERVER_ERROR), 'Internal server error.' )
Did you see that the Swagger UI uses info in these decorators in the page?
Now go inside class TreeList -- we have two main methods:
def get( self ):
def post( self ):
which're API methods -- they call to methods defined in bro.py, discussed earlier, to do the work. As can be seen, these methods can also have decorators which define additional response codes and associated messages.
@tree_ns.expect( create_tree_reqparser )
The above decorator is explained under class flask_restx.Namespace( ... ).
Finally, we need to make the new codes effective. We need to update the main API module:
File f:\flask_restx_demo\src\flask_restx_demo\api\__init__.py -- final version
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
""" Flask-RESTX API blueprint configuration. """
from flask import Blueprint
from flask_restx import Api
from flask_restx_demo.api.trees.routes import tree_ns
api_bp = Blueprint( 'api', __name__, url_prefix='/api/v1' )
api = Api(
api_bp,
version='1.0',
title='Flask-RESTX API Demo',
description='Welcome to Flask-RESTX API with Swagger UI documentation',
doc='/ui',
)
api.add_namespace( tree_ns, path='/trees' )
There’re only two new lines added: line 5 and line 17.
That's about it for this post 😆. I wasn't sure if it's worth writing or not... I must say I wasn't very enthusiastic about writing it: since the codes are mediocre and not every exciting. I like to learn stuff in incremental steps... This is one of these steps. So I thought I would write this one for me... I'm sorry it is a bit long, but as I'm writing it, more and more stuff seems to need explanations. I hope I did not make any mistakes in the codes and in the post. This's merely scratching the surface... There're still much more to the subject. Thank you for reading and I hope you find this one useful.
Updated 14/July/2022: Final code for this post can be cloned from GitHub using:
git clone -b v1.0.0 https://github.com/behai-nguyen/flask-restx-demo.git