Generate Automated RestfulAPI Documentation Using Swagger and Flask
Swagger is one of the Open API Specification tools we could use to generate automated documentation for our APIs.
If you are still confused about what Open API Specification is, you could read my previous article here: What is Open API Specification and Why You Should Put It To Your RestfulAPI?
To implement Swagger on our Flask App, we need to install a library named flask-restx.
Flask-RESTX is an open-source library that supports quick setup for RestfulAPI development. It's a fork project from Flask-RESTPlus.
Why Flask-RESTX and Not Flask-RESTPlus?
Well, Flask-RESTPlus is already unmaintained. Noirbizarre as the original writer of the project already abandon this project and the other contributor has no access to the PyPi project. So they fork Flask-RESTPlus and change it to Flask-RESTX to keep this project Alive. Flask-RESTX also already supports Flask 2. x.
SETUP.
Create a project with the following structure.
Models containing data models to describe our RestAPI data structure.
Routes containing API endpoints for each module.
Now, before we code, install the following libraries.
Flask==2.2.2
flask-restx==1.0.3
PyJWT==2.6.0
Create a Simple RestAPI
In this tutorial, we gonna create RestfulAPI with dummy data, so it won't use any database. Providing databases required some extra effort, which is not the objective of this tutorial.
Lets code.
Write the following code inside the models > cat.py files.
from flask_restx import fields
class CatModel:
def __init__(self, namespace):
self.namespace=namespace
def get_cats(self):
data_model={
"data":fields.List(
fields.Nested(self._a_cat())
),
"message":fields.String()
}
return self.namespace.model('get_cats_model',data_model )
def _a_cat(self):
data_model={
"id":fields.String(),
"name":fields.String()
}
return self.namespace.model('a_cat_model',data_model )
Here we have CatModel to define the data structure for our Cat APIs.
Create the API Endpoint inside routes > cat.py
from flask_restx import Namespace, Resource
from models.cat import CatModel
api = Namespace('Cats',"APIs related to cat modules")
cat_model=CatModel(api)
cat_dummy_data=[
{'id': '1', 'name': 'Whiskers'},
{'id': '2', 'name': 'Fluffy'},
{'id': '3', 'name': 'Shawn'}
]
@api.route('/')
class CatResource(Resource):
@api.marshal_with(cat_model.get_cats())
def get(self):
"List all cats"
return {"data":cat_dummy_data, "message":"OK"}, 200
Here, we create an API to show the cat list.
‘Namespace’ is similar to Blueprint in Flask. You could split your API Endpoint and make it more modular.
If you take a look at the following code, you could see that @api.route(‘/’) is not receiving a function, but a class.
@api.route('/')
class CatResource(Resource):
def get(self):
...
Well, it's a feature from Flask-RESTFul, a dependency of Flask-RESTX, it allows you to split your endpoint method by the function name. get stands for get method, post for post method, and so on.
‘marshal_with’ are used to filter and wrap our response data. Swagger will describe our API response by reading the marshall object.
Don't forget to create a doc string in your API function, swagger will detect this as the API endpoint description.
...
def get(self):
"List all cats"
...
Register the namespace and create the Flask app inside main.py files.
from flask import Flask
from flask_restx import Api
from routes.cat import api as cat_namespace
api = Api(
title="Pets Shop API",
version="1.0",
description="API for Pets Shop App",
doc="/doc"
)
api.add_namespace(cat_namespace, path='/cat')
app =Flask(__name__)
app.config["RESTX_MASK_SWAGGER"]=False
api.init_app(app)
if __name__=="__main__":
app.run(debug=True, port=5000)
Here, we write our swagger configuration. We set the documentation at /doc endpoints.
...
api = Api(
title="Pets Shop API",
version="1.0",
description="API for Pets Shop App",
doc="/doc"
)
...
app.config[“RESTX_MASK_SWAGGER”]=False is used to deactivate the default mask parameter.
Now, run the program and go to http://127.0.0.1:5000/doc.
You will see the swagger UI like this:
Fantastic. But this is very basic, when dealing with real API, the case becomes more complex.
Let's create another API to simulate them.
Add the following code to models > cat.py.
...
from flask_restx import reqparse
class CatModel:
...
def get_cats_expected_params(self):
parser=reqparse.RequestParser()
parser.add_argument("cat_name", type=str, help="Optional filter by cat names", location="args" )
return parser
def add_new_cat_expected_payload(self):
data_model={
"cat_name":fields.String(required=True, description="The cat name")
}
return self.namespace.model('add_cat_expected_payload',data_model )
def add_new_cat(self):
data_model={
"data":fields.Nested(self._a_cat()),
"message":fields.String()
}
return self.namespace.model('add_new_cat',data_model )
def get_cat(self):
data_model={
"data":fields.Nested(self._a_cat()),
"message":fields.String()
}
return self.namespace.model('get_cat',data_model )
Update the APIs inside routes > cat.py files.
...
from flask import request
...
@api.route('/')
class CatResource(Resource):
@api.expect(cat_model.get_cats_expected_params())
@api.marshal_with(cat_model.get_cats())
def get(self):
"List all cats"
filter_cat_name=request.args.get("cat_name", "")
if not filter_cat_name:
return {"data":cat_dummy_data, "message":"OK"}, 200
cats=[]
for cat in cat_dummy_data :
if cat.get("name").lower() == filter_cat_name.lower():
cats.append(cat)
if cats:
return {"data":cats, "message":"OK"}, 200
return {"data":cats, "message":"Cats is not found"}, 404
@api.expect(cat_model.add_new_cat_expected_payload())
@api.marshal_with(cat_model.add_new_cat())
def post(self):
"Add Cat"
new_cat={
"id":str( int(cat_dummy_data[-1].get('id')) +1 )
"name":request.json.get("cat_name")
}
cat_dummy_data.append(new_cat)
return {"data":new_cat, "message":"OK"}, 200
@api.route('/<cat_id>/')
class CatList(Resource):
@api.marshal_with(cat_model.get_cat())
def get(self, cat_id):
"Get cat"
for cat in cat_dummy_data:
if cat.get("id") == cat_id:
return {"data":cat, "message":"OK"}, 200
return {"data":{}, "message":"Cat is not found"}, 404
Now if you run the program, you will see there are 3 API endpoints right now.
If you take a look at List All Cats API, there is a parameter for the API.
To specify the Parameter, we could use the request parser from Flask-RESTX.
...
from flask_restx import reqparse
class CatModel:
...
def get_cats_expected_params(self):
parser=reqparse.RequestParser()
parser.add_argument("cat_name", type=str, help="Optional filter by cat names", location="args" )
return parser
On the post method, we need to tell the user to put data inside the request body.
We could make a new model for this.
def add_new_cat_expected_payload(self):
data_model={
"cat_name":fields.String(required=True, description="The cat name")
}
return self.namespace.model('add_cat_expected_payload',data_model )
For URL variables, we don't need to specify anything, swagger will automatically detect this.
All of those parameters should be put inside api.expect() decorator.
...
@api.expect(cat_model.get_cats_expected_params())
@api.marshal_with(cat_model.get_cats())
def get(self):
...
Right, we already created a nice RestfulAPI.
But what about security? RestfulAPI usually has an authorization method, such likes a token, session, OAuth, etc. Swagger already supports API key and OAuth documentation also.
Let's create a simple login API and write an authorization decorator to check the login status through the access token.
Create a new file inside the ‘models’ folder, named login.py.
from flask_restx import fields
class LoginModel:
def __init__(self, namespace):
self.namespace=namespace
def login_expected_payload(self):
data_model={
"username":fields.String(),
"password":fields.String(),
}
return self.namespace.model("login_expected_payload", data_model)
def login(self):
data_model={
"data":fields.Nested(self._token()),
"message":fields.String()
}
return self.namespace.model("login", data_model)
def _token(self):
data_model={
"token":fields.String()
}
return self.namespace.model("token", data_model)
Create a new file inside the routes folder, named login.py.
from flask import request
from flask_restx import Namespace, Resource
from models.login import LoginModel
import jwt
api = Namespace('Login',"APIs related to Login Auth")
login_model=LoginModel(api)
default_user={
"username":"admin",
"password":"admin"
}
jwt_secret_key="this is screet key to generate jwt token"
@api.route('/')
class Credential(Resource):
@api.expect(login_model.login_expected_payload())
@api.marshal_with(login_model.login())
def post(self):
"Login"
is_identical_username= default_user.get('username') == request.json.get("username")
is_identical_password= default_user.get('password') == request.json.get("password")
token=None
if is_identical_username and is_identical_password:
token=jwt.encode(
{"username":default_user.get('username')},
jwt_secret_key,
algorithm="HS256"
)
if token:
return {"data":{"token":token}, "message":"OK"}, 200
return {"data":{"token":token}, "message":"Incorrect username or password"}, 500
Create a new file named auth at the same level as main.py.
import jwt
from flask import request
from functools import wraps
jwt_secret_key="this is screet key to generate jwt token"
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
token=request.headers.get("token")
if not token:
return {"message":'Forbidden access'}
try:
jwt.decode(token, jwt_secret_key, algorithms='HS256')
except jwt.ExpiredSignatureError:
return {"message":'Forbidden access'}
except:
return {"message":'Invalid Token'}
return func(*args, **kwargs)
return wrapper
This is what the project looks like now:
Update the main.py files.
...
from routes.login import api as login_namespace
api = Api(
...
authorizations={
"apikey":{
"type":"apiKey",
"in":"header",
"name":"token"
}
}
)
...
api.add_namespace(login_namespace, path='/login')
...
Here we describe that we gonna use apikey as an authorization method and it will be sent in the request header with a key named ‘token’.
Update the routes > cat.py files.
...
from auth import login_required
...
@api.route('/')
class CatResource(Resource):
@api.doc(security="apikey")
@login_required
@api.expect(cat_model.get_cats_expected_params())
@api.marshal_with(cat_model.get_cats())
def get(self):
"List all cats"
...
@api.doc(security="apikey")
@login_required
@api.expect(cat_model.add_new_cat_expected_payload())
@api.marshal_with(cat_model.add_new_cat())
def post(self):
"Add Cat"
....
@api.route('/<cat_id>/')
class CatList(Resource):
@api.doc(security="apikey")
@login_required
@api.marshal_with(cat_model.get_cat())
def get(self, cat_id):
"Get cat"
...
Check the API.
Now the swagger UI will have an authorize button. If we click the button, it will show a form to submit an auth token.
If we try to access the API without a token, the API will return a forbidden access response.
Now let's try to log in first.
We will receive the jwt token.
Put it on the authorization form.
Try to access the API again.
Very nice. We have created a complete RestAPI with Swagger API documentation on it.
Thanks for reading my article. Leave a clap if you like it 👏🏻.
If you want to take a lot at the full source code, you could access it at my GitHub repository here.