Flask is a very popular and powerful framework for building web applications. Over the last years, people used it to create REST API that work well with decoupled, modern front-end applications.
One challenge that backend development teams often face, is how to make it easy for front-end developers, wethere internal or with a distant community, to create API-compliant clients (web app, mobile app or even CLI tools…)
In the wild, they are many good examples of well-documented APIs… Take the Twitter API : the docs are great, user-friendly and cover all the available endpoint with tips and examples. Any fresh CS student could write a small Python tool using it, just by following the documentation and its examples.
At @Ooreka, we decided to follow the OpenAPI (fka Swagger 2.0) specification to build a solid documentation for our Flask-powered micro-services APIs. Let’s dive in.
3..2..1.. Doc!
Thanks to the apispec lib, you can automagically generate a specification file (commonly named swagger.json
) form your Flask code. Some other libraries can do a lot of magic for you, but apispec
is really simple to use and can sit next to your code without interfering with it.
It supports Marshmallow and Flask, allowing you to re-use your code to generate a proper documentation !
Let’s write our generation script, e.g. app/scripts/openapi.py
:
<span>from</span> <span>apispec</span> <span>import</span> <span>APISpec</span><span># Create spec </span><span>spec</span> <span>=</span> <span>APISpec</span><span>(</span><span>title</span><span>=</span><span>'My Awesome API'</span><span>,</span><span>version</span><span>=</span><span>'1.0.42'</span><span>,</span><span>info</span><span>=</span><span>dict</span><span>(</span><span>description</span><span>=</span><span>'You know, for devs'</span><span>),</span><span>plugins</span><span>=</span><span>[</span><span>'apispec.ext.flask'</span><span>,</span><span>'apispec.ext.marshmallow'</span><span>]</span><span>)</span><span># Reference your schemas definitions </span><span>from</span> <span>app.schemas</span> <span>import</span> <span>FooSchema</span><span>spec</span><span>.</span><span>definition</span><span>(</span><span>'Foo'</span><span>,</span> <span>schema</span><span>=</span><span>FooSchema</span><span>)</span><span># ... </span><span># Now, reference your routes. </span><span>from</span> <span>app.views</span> <span>import</span> <span>my_route</span><span># We need a working context for apispec introspection. </span><span>app</span> <span>=</span> <span>create_app</span><span>()</span><span>with</span> <span>app</span><span>.</span><span>test_request_context</span><span>():</span><span>spec</span><span>.</span><span>add_path</span><span>(</span><span>view</span><span>=</span><span>my_route</span><span>)</span><span># ... </span><span># We're good to go! Save this to a file for now. </span><span>with</span> <span>open</span><span>(</span><span>'swagger.json'</span><span>,</span> <span>'w'</span><span>)</span> <span>as</span> <span>f</span><span>:</span><span>json</span><span>.</span><span>dump</span><span>(</span><span>spec</span><span>.</span><span>to_dict</span><span>(),</span> <span>f</span><span>)</span><span>from</span> <span>apispec</span> <span>import</span> <span>APISpec</span> <span># Create spec </span><span>spec</span> <span>=</span> <span>APISpec</span><span>(</span> <span>title</span><span>=</span><span>'My Awesome API'</span><span>,</span> <span>version</span><span>=</span><span>'1.0.42'</span><span>,</span> <span>info</span><span>=</span><span>dict</span><span>(</span> <span>description</span><span>=</span><span>'You know, for devs'</span> <span>),</span> <span>plugins</span><span>=</span><span>[</span> <span>'apispec.ext.flask'</span><span>,</span> <span>'apispec.ext.marshmallow'</span> <span>]</span> <span>)</span> <span># Reference your schemas definitions </span><span>from</span> <span>app.schemas</span> <span>import</span> <span>FooSchema</span> <span>spec</span><span>.</span><span>definition</span><span>(</span><span>'Foo'</span><span>,</span> <span>schema</span><span>=</span><span>FooSchema</span><span>)</span> <span># ... </span> <span># Now, reference your routes. </span><span>from</span> <span>app.views</span> <span>import</span> <span>my_route</span> <span># We need a working context for apispec introspection. </span><span>app</span> <span>=</span> <span>create_app</span><span>()</span> <span>with</span> <span>app</span><span>.</span><span>test_request_context</span><span>():</span> <span>spec</span><span>.</span><span>add_path</span><span>(</span><span>view</span><span>=</span><span>my_route</span><span>)</span> <span># ... </span> <span># We're good to go! Save this to a file for now. </span><span>with</span> <span>open</span><span>(</span><span>'swagger.json'</span><span>,</span> <span>'w'</span><span>)</span> <span>as</span> <span>f</span><span>:</span> <span>json</span><span>.</span><span>dump</span><span>(</span><span>spec</span><span>.</span><span>to_dict</span><span>(),</span> <span>f</span><span>)</span>from apispec import APISpec # Create spec spec = APISpec( title='My Awesome API', version='1.0.42', info=dict( description='You know, for devs' ), plugins=[ 'apispec.ext.flask', 'apispec.ext.marshmallow' ] ) # Reference your schemas definitions from app.schemas import FooSchema spec.definition('Foo', schema=FooSchema) # ... # Now, reference your routes. from app.views import my_route # We need a working context for apispec introspection. app = create_app() with app.test_request_context(): spec.add_path(view=my_route) # ... # We're good to go! Save this to a file for now. with open('swagger.json', 'w') as f: json.dump(spec.to_dict(), f)
Enter fullscreen mode Exit fullscreen mode
Here, we first create a new APISpec instance with some details about our API.
Then, we add our definitions (here, we are using Marshmallow to define how our API will serialize/deserialize data) with APISpec.definition()
.
Finally, we add our routes to our API specification using APISpec.add_path()
. apispec
will parse your route functions docstrings, so make sure your add some OpenAPI YaML stuff here, as in :
<span>@</span><span>app</span><span>.</span><span>route</span><span>(</span><span>'/foo/<bar_id>'</span><span>)</span><span>def</span> <span>my_route</span><span>(</span><span>gist_id</span><span>):</span><span>""" Cool Foo-Bar route. - - - # dev.to editor dislike triple hyphen, be sure to remove spaces here. get: summary: Foo-Bar endpoint. description: Get a single foo with the bar ID. parameters: - name: bar_id in: path description: Bar ID type: integer required: true responses: 200: description: Foo object to be returned. schema: FooSchema 404: description: Foo not found. """</span><span># (...) </span> <span>return</span> <span>jsonify</span><span>(</span><span>foo</span><span>)</span><span>@</span><span>app</span><span>.</span><span>route</span><span>(</span><span>'/foo/<bar_id>'</span><span>)</span> <span>def</span> <span>my_route</span><span>(</span><span>gist_id</span><span>):</span> <span>""" Cool Foo-Bar route. - - - # dev.to editor dislike triple hyphen, be sure to remove spaces here. get: summary: Foo-Bar endpoint. description: Get a single foo with the bar ID. parameters: - name: bar_id in: path description: Bar ID type: integer required: true responses: 200: description: Foo object to be returned. schema: FooSchema 404: description: Foo not found. """</span> <span># (...) </span> <span>return</span> <span>jsonify</span><span>(</span><span>foo</span><span>)</span>@app.route('/foo/<bar_id>') def my_route(gist_id): """ Cool Foo-Bar route. - - - # dev.to editor dislike triple hyphen, be sure to remove spaces here. get: summary: Foo-Bar endpoint. description: Get a single foo with the bar ID. parameters: - name: bar_id in: path description: Bar ID type: integer required: true responses: 200: description: Foo object to be returned. schema: FooSchema 404: description: Foo not found. """ # (...) return jsonify(foo)
Enter fullscreen mode Exit fullscreen mode
You will end up with a valid JSON API specification. Now, let’s see how to bootstrap an HTML version to show it to the world!
Browerify-ing all this
A really cool tool to do that is the ReDoc Javascript library from the guys at APIs.guru. We’ll use it to present the generated JSON specification in a convenient way.
Redoc is basically a single, minified JS file you can include in a bare index.html
file and tell it where your swagger.json
is located. It uses a really neat 3 columns design : a navigation sidebar, a wide center section with your API endpoints definitions and a third column dedicated to requests or responses samples and examples.
<span><!DOCTYPE html></span><span><html></span><span><head></span><span><title></span>Cool API Documentation<span></title></span><span><meta</span> <span>charset=</span><span>"utf-8"</span><span>/></span><span><meta</span> <span>name=</span><span>"viewport"</span> <span>content=</span><span>"width=device-width, initial-scale=1"</span><span>></span><span><style></span> <span>body</span> <span>{</span> <span>margin</span><span>:</span> <span>0</span><span>;</span> <span>padding</span><span>:</span> <span>0</span><span>;</span> <span>}</span> <span></style></span><span></head></span><span><body></span><span><redoc</span> <span>spec-url=</span><span>'./swagger.json'</span> <span>hide-loading</span><span>></redoc></span><span><script </span><span>src=</span><span>"https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"</span><span>></span> <span></script></span><span></body></span><span></html></span><span><!DOCTYPE html></span> <span><html></span> <span><head></span> <span><title></span>Cool API Documentation<span></title></span> <span><meta</span> <span>charset=</span><span>"utf-8"</span><span>/></span> <span><meta</span> <span>name=</span><span>"viewport"</span> <span>content=</span><span>"width=device-width, initial-scale=1"</span><span>></span> <span><style></span> <span>body</span> <span>{</span> <span>margin</span><span>:</span> <span>0</span><span>;</span> <span>padding</span><span>:</span> <span>0</span><span>;</span> <span>}</span> <span></style></span> <span></head></span> <span><body></span> <span><redoc</span> <span>spec-url=</span><span>'./swagger.json'</span> <span>hide-loading</span><span>></redoc></span> <span><script </span><span>src=</span><span>"https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"</span><span>></span> <span></script></span> <span></body></span> <span></html></span><!DOCTYPE html> <html> <head> <title>Cool API Documentation</title> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> body { margin: 0; padding: 0; } </style> </head> <body> <redoc spec-url='./swagger.json' hide-loading></redoc> <script src="https://rebilly.github.io/ReDoc/releases/latest/redoc.min.js"> </script> </body> </html>
Enter fullscreen mode Exit fullscreen mode
Yep, that was quick. Check out a real-world example here.
Wrapping it up
The OpenAPI offers many options I didn’t cover here for brievity and simplification. You can add your server’s real endpoints to the doc, add many details about the parameters and responses of your routes, provide example in your routes functions docstring that will be parsed and added to your spec, etc…
As a final tip, head to the Flask CLI documentation to see how easily you can hook your generation script into the command line interface of Flask (this will give you some badass command like FLASK_APP=main.py flask generate_doc
). Oh, and be sure to put this into your Continuous Integration routine to keep your API documentation up-to-date with your API!
Cheers!
NOTE: This post was originally posted on Medium.
暂无评论内容