Documenting your API with APIFairy¶
APIFairy can discover and document your API through its decorators, but in most cases you’ll want to complement automatically generated documentation with manually written notes. The following sections describe all the places where APIFairy looks for text to attach to your project’s documentation.
Project Title and Version¶
The title and version of your project are defined in the Flask configuration object:
app = Flask(__name__)
app.config['APIFAIRY_TITLE'] = 'My API Project'
app.config['APIFAIRY_VERSION'] = '1.0'
Project Overview¶
Most API documentation sites include one or more sections that provide general project information for developers, such as how to authenticate, how pagination works, or what is the structure of error responses. APIFairy looks for project description text to attach to the documentation in module-level docstrings in all the packages and modules referenced in the Flask application’s import name, starting from the right side.
While different OpenAPI documentation renderers may have different expectations for the formatting of this text, it is fairly common for documentation to be written in Markdown format, with support for long, multi-line text.
To help clarify how this works, consider a project with the following structure:
- my_api_project/
- api/
__init__.py
app.py
routes.py
project.py
The contents of project.py are:
from api.app import create_app
app = create_app()
The contents of api/app.py are:
from flask import Flask
from apifairy import APIFairy
apifairy = APIFairy()
def create_app():
app = Flask(__name__)
app.config['APIFAIRY_TITLE'] = 'My API Project'
app.config['APIFAIRY_VERSION'] = '1.0'
apifairy.init_app(app)
return app
With this project structure, the import name of the Flask application is
api.app
. In general, the import name of the application is the value that
is passed as first argument to the Flask
class. In most cases this is the
__name__
Python global variable, which represents the fully qualified
package name of the module in which the application is defined.
Following this example, APIFairy will first look for project-level
documentation in the api.app
module, which maps to the api/app.py file.
Documentation can then be added at the top of this file, as follows:
"""Welcome to My API Project!
## Project Overview
This is the project overview.
## Authentication
This is how authentication works.
"""
from flask import Flask
from apifairy import APIFairy
apifairy = APIFairy()
def create_app():
app = Flask(__name__)
app.config['APIFAIRY_TITLE'] = 'My API Project'
app.config['APIFAIRY_VERSION'] = '1.0'
apifairy.init_app(app)
return app
If APIFairy does not find a module docstring in api.app
, it will remove the
last component of the import name and try again. Following this example, this
would be api
, which is a package, so its docstring can be found in
api/__init__.py.
So the alternative to putting the documentation in api/app.py is to leave this file without a docstring, and instead add the documentation in api/__init__.py.
Endpoints¶
To document an endpoint, add a docstring to its view function. The first line of the docstring should be a short summary of the endpoint’s purpose. A longer description can be included starting from the second line.
Example with just a summary:
@users.route('/users', methods=['POST'])
@body(user_schema)
@response(user_schema, 201)
def new(args):
"""Register a new user"""
user = User(**args)
db.session.add(user)
db.session.commit()
return user
Example with summary and longer description:
@users.route('/users', methods=['POST'])
@body(user_schema)
@response(user_schema, 201)
def new(args):
"""Register a new user
Clients can use this endpoint when they need to register a new user
in the system.
"""
user = User(**args)
db.session.add(user)
db.session.commit()
return user
As with the project overview, these docstrings can also be written in Markdown.
Path parameters¶
For endpoints that have dynamic components in their path, APIFairy will automatically extract their type directly from the Flask route specification. A text description of a parameter can be included by adding a string as an annotation.
Annotations have been evolving in recent releasees of Python, so the best format to provide documentation for endpoint parameters depends on which version of Python you are using.
The basic method, which works with any recent version of Python, involves simply adding the documentation as a string annotation to the parameter:
@users.route('/users/<int:id>', methods=['GET'])
@authenticate(token_auth)
@response(user_schema)
def get(id: 'The id of the user to retrieve.'): # noqa: F722
"""Retrieve a user by id"""
return db.session.get(User, id) or abort(404)
While this method works, Python code linters and type checkers will flag the
annotation as invalid, because they expect annotations to be used for type
hints and not for documentation, so it may be necessary to add a noqa
or
similar comment for these errors to be ignored.
If using Python 3.9 or newer, luckily there is a better option. The typing.Annotated type can be used to provide a type hint for the parameter along with additional metadata such as a documentation string:
from typing import Annotated
@users.route('/users/<int:id>', methods=['GET'])
@authenticate(token_auth)
@response(user_schema)
def get(id: Annotated[int, 'The id of the user to retrieve.']):
"""Retrieve a user by id"""
return db.session.get(User, id) or abort(404)
Even if the project does not use type hints, using this format will prevent linting and typing errors, so it is the preferred way to document a parameter.
Documentation for parameters can include multiple lines and paragraphs, if desired. Markdown formatting is also supported by most OpenAPI renderers.
Schemas¶
Many of the APIFairy decorators accept Marshmallow schemas as arguments. These schemas are automatically documented, including their field types and validation requirements.
If the application wants to provide additional information, a schema
description can be provided in the description
field of the schema’s
metaclass:
class UserSchema(ma.SQLAlchemySchema):
class Meta:
model = User
ordered = True
description = 'This schema represents a user.'
id = ma.auto_field(dump_only=True)
url = ma.String(dump_only=True)
username = ma.auto_field(required=True,
validate=validate.Length(min=3, max=64))
Documentation that is specific to a schema field can be added in a
description
argument when the field is declared:
class UserSchema(ma.SQLAlchemySchema):
class Meta:
model = User
ordered = True
id = ma.auto_field(dump_only=True, description="The user's id.")
url = ma.String(dump_only=True, description="The user's unique URL.")
username = ma.auto_field(required=True,
validate=validate.Length(min=3, max=64),
description="The user's username.")
Query String¶
APIFairy will automatically document query string parameters for endpoints that use the @arguments decorator:
@users.route('/users', methods=['GET'])
@arguments(pagination_schema)
@response(users_schema)
def get_users(pagination):
"""Retrieve all users"""
# ...
Request Headers¶
APIFairy also documents request headers that are declared with the @arguments decorator. Note that this decorator defaults to the query string, but the location argument can be set to headers when needed.
Example:
class HeadersSchema(ma.Schema):
x_token = ma.String(data_key='X-Token', required=True)
@users.route('/users', methods=['GET'])
@arguments(HeadersSchema, location='headers')
@response(users_schema)
def get_users(headers):
"""Retrieve all users"""
# ...
The @arguments
decorator can be given twice when an endpoint needs query
string and header arguments both:
@users.route('/users', methods=['GET'])
@arguments(PaginationSchema)
@arguments(HeadersSchema, location='headers')
@response(users_schema)
def all(pagination, headers):
"""Retrieve all users"""
# ...
Responses¶
In addition to the schema documentation, an endpoint response can be given a
text description in a description
argument to the @response
decorator.
Example:
@tokens.route('/tokens', methods=['PUT'])
@body(token_schema)
@response(token_schema, description='Newly issued access and refresh tokens')
def refresh(args):
"""Refresh an access token"""
...
For endpoints that return information in response headers, the headers
argument can be used to add these to the documentation:
class HeadersSchema(ma.Schema):
x_token = ma.String(data_key='X-Token')
@tokens.route('/tokens', methods=['PUT'])
@body(token_schema)
@response(token_schema, headers=HeadersSchema)
def refresh(args):
"""Refresh an access token"""
...
Error Responses¶
The @other_responses
decorator takes a dictionary argument, where the keys
are the response status codes and the values provide the documentation.
To add text descriptions to these responses, set the value for each status code to a descrition string.
Example:
@tokens.route('/tokens', methods=['PUT'])
@body(token_schema)
@response(token_schema, description='Newly issued access and refresh tokens')
@other_responses({401: 'Invalid access or refresh token',
403: 'Insufficient permissions'})
def refresh(args):
"""Refresh an access token"""
...
To document the error response with a schema, set the value to the schema instance.
Example:
@tokens.route('/tokens', methods=['PUT'])
@body(token_schema)
@response(token_schema, description='Newly issued access and refresh tokens')
@other_responses({401: invalid_token_schema,
403: insufficient_permissions_schema})
def refresh(args):
"""Refresh an access token"""
...
A schema and a description can both be given as a tuple:
@tokens.route('/tokens', methods=['PUT'])
@body(token_schema)
@response(token_schema, description='Newly issued access and refresh tokens')
@other_responses({401: (invalid_token_schema, 'Invalid access or refresh token'),
403: (insufficient_permissions_schema, 'Insufficient permissions')})
def refresh(args):
"""Refresh an access token"""
...
Authentication¶
APIFairy recognizes the Flask-HTTPAuth authentication object passed to the
@authenticate
decorator and creates the appropriate structure according to
the OpenAPI specification. To add textual documentation, define a subclass of
the Flask-HTTPAuth authentication object and add a docstring with the
documentation to it.
Example:
from flask_httpauth import HTTPBasicAuth
class DocumentedAuth(HTTPBasicAuth):
"""Basic authentication scheme."""
pass
basic_auth = DocumentedAuth()
@tokens.route('/tokens', methods=['POST'])
@authenticate(basic_auth)
@response(token_schema)
@other_responses({401: 'Invalid username or password'})
def new():
"""Create new access and refresh tokens"""
...
Anything else¶
For any other documentation needs that are not covered by the options listed
above, the application can manually modify the OpenAPI structure. This can be
achieved in a function decorated with the @process_apispec
decorator:
@apifairy.process_apispec
def my_apispec_processor(spec):
# modify spec as needed here
return spec