Build and document REST APIs with aiohttp and apispec
aiohttp-apispec key features:
docsandrequest_schemadecorators to add swagger spec support out of the box;validation_middlewaremiddleware to enable validating with marshmallow schemas from those decorators;- SwaggerUI support.
 - New from version 2.0 -  
match_info_schema,querystring_schema,form_schema,json_schema,headers_schemaandcookies_schemadecorators for specific request parts validation. Look here for more info. 
aiohttp-apispec api is fully inspired by flask-apispec library
Version 3.0.0b1 with apispec>=5.0 webargs>=8.0 is in beta now (pip install aiohttp-apispec==3.0.0b1).
- Install
 - Quickstart
 - Adding validation middleware
 - More decorators
 - Custom error handling
 - Build swagger web client
 - Versioning
 
pip install aiohttp-apispec
Also you can read blog post about quickstart with aiohttp-apispec
from aiohttp_apispec import (
    docs,
    request_schema,
    setup_aiohttp_apispec,
)
from aiohttp import web
from marshmallow import Schema, fields
class RequestSchema(Schema):
    id = fields.Int()
    name = fields.Str(description="name")
@docs(
    tags=["mytag"],
    summary="Test method summary",
    description="Test method description",
)
@request_schema(RequestSchema(strict=True))
async def index(request):
    return web.json_response({"msg": "done", "data": {}})
app = web.Application()
app.router.add_post("/v1/test", index)
# init docs with all parameters, usual for ApiSpec
setup_aiohttp_apispec(
    app=app, 
    title="My Documentation", 
    version="v1",
    url="/api/docs/swagger.json",
    swagger_path="/api/docs",
)
# Now we can find spec on 'http://localhost:8080/api/docs/swagger.json'
# and docs on 'http://localhost:8080/api/docs'
web.run_app(app)Class based views are also supported:
class TheView(web.View):
    @docs(
        tags=["mytag"],
        summary="View method summary",
        description="View method description",
    )
    @request_schema(RequestSchema(strict=True))
    @response_schema(ResponseSchema(), 200)
    def delete(self):
        return web.json_response(
            {"msg": "done", "data": {"name": self.request["data"]["name"]}}
        )
app.router.add_view("/v1/view", TheView)As alternative you can add responses info to docs decorator, which is more compact way.
And it allows you not to use schemas for responses documentation:
@docs(
    tags=["mytag"],
    summary="Test method summary",
    description="Test method description",
    responses={
        200: {
            "schema": ResponseSchema,
            "description": "Success response",
        },  # regular response
        404: {"description": "Not found"},  # responses without schema
        422: {"description": "Validation error"},
    },
)
@request_schema(RequestSchema(strict=True))
async def index(request):
    return web.json_response({"msg": "done", "data": {}})from aiohttp_apispec import validation_middleware
...
app.middlewares.append(validation_middleware)Now you can access all validated data in route from request['data'] like so:
@docs(
    tags=["mytag"],
    summary="Test method summary",
    description="Test method description",
)
@request_schema(RequestSchema(strict=True))
@response_schema(ResponseSchema, 200)
async def index(request):
    uid = request["data"]["id"]
    name = request["data"]["name"]
    return web.json_response(
        {"msg": "done", "data": {"info": f"name - {name}, id - {uid}"}}
    )You can change Request's 'data' param to another with request_data_name argument of
setup_aiohttp_apispec function:
setup_aiohttp_apispec(
    app=app,
    request_data_name="validated_data",
)
...
@request_schema(RequestSchema(strict=True))
async def index(request):
    uid = request["validated_data"]["id"]
    ...Also you can do it for specific view using put_into
parameter (beginning from version 2.0):
@request_schema(RequestSchema(strict=True), put_into="validated_data")
async def index(request):
    uid = request["validated_data"]["id"]
    ...Starting from version 2.0 you can use shortenings for documenting and validating specific request parts like cookies, headers etc using those decorators:
| Decorator name | Default put_into param | 
|---|---|
| match_info_schema | match_info | 
| querystring_schema | querystring | 
| form_schema | form | 
| json_schema | json | 
| headers_schema | headers | 
| cookies_schema | cookies | 
And example:
@docs(
    tags=["users"],
    summary="Create new user",
    description="Add new user to our toy database",
    responses={
        200: {"description": "Ok. User created", "schema": OkResponse},
        401: {"description": "Unauthorized"},
        422: {"description": "Validation error"},
        500: {"description": "Server error"},
    },
)
@headers_schema(AuthHeaders)  # <- schema for headers validation
@json_schema(UserMeta)  # <- schema for json body validation
@querystring_schema(UserParams)  # <- schema for querystring params validation
async def create_user(request: web.Request):
    headers = request["headers"]  # <- validated headers!
    json_data = request["json"]  # <- validated json!
    query_params = request["querystring"]  # <- validated querystring!
    ...If you want to catch validation errors by yourself you
could use error_callback parameter and create your custom error handler. Note that
it can be one of coroutine or callable and it should
have interface exactly like in examples below:
from marshmallow import ValidationError, Schema
from aiohttp import web
from typing import Optional, Mapping, NoReturn
def my_error_handler(
    error: ValidationError,
    req: web.Request,
    schema: Schema,
    error_status_code: Optional[int] = None,
    error_headers: Optional[Mapping[str, str]] = None,
) -> NoReturn:
    raise web.HTTPBadRequest(
            body=json.dumps(error.messages),
            headers=error_headers,
            content_type="application/json",
        )
setup_aiohttp_apispec(app, error_callback=my_error_handler)Also you can create your own exceptions and create regular Request in middleware like so:
class MyException(Exception):
    def __init__(self, message):
        self.message = message
# It can be coroutine as well:
async def my_error_handler(
    error, req, schema, error_status_code, error_headers
):
    await req.app["db"].do_smth()  # So you can use some async stuff
    raise MyException({"errors": error.messages, "text": "Oops"})
# This middleware will handle your own exceptions:
@web.middleware
async def intercept_error(request, handler):
    try:
        return await handler(request)
    except MyException as e:
        return web.json_response(e.message, status=400)
setup_aiohttp_apispec(app, error_callback=my_error_handler)
# Do not forget to add your own middleware before validation_middleware
app.middlewares.extend([intercept_error, validation_middleware])Just add swagger_path parameter to setup_aiohttp_apispec function.
For example:
setup_aiohttp_apispec(app, swagger_path="/docs")Then go to /docs and see awesome SwaggerUI
If you prefer older version you can use
aiohttp_swagger library.
aiohttp-apispec adds swagger_dict parameter to aiohttp web application
after initialization (with setup_aiohttp_apispec function).
So you can use it easily like:
from aiohttp_apispec import setup_aiohttp_apispec
from aiohttp_swagger import setup_swagger
def create_app(app):
    setup_aiohttp_apispec(app)
    async def swagger(app):
        setup_swagger(
            app=app, swagger_url="/api/doc", swagger_info=app["swagger_dict"]
        )
    app.on_startup.append(swagger)
    # now we can access swagger client on '/api/doc' url
    ...
    return appThis software follows Semantic Versioning.
Please star this repository if this project helped you!