OpenAPI has become the de facto standard of designing web APIs, and there are numerous tools developed around its ecosystem. In this article, I will demonstrate the workflow of using OpenAPI in both backend and frontend projects.
API Server
There are code first and design first approaches when using OpenAPI, and here we go with code first approach, i.e. writing the API server first, add specification to the method docs, then generate the final OpenAPI specification. The API server will be developed with Python Flask framework and apispec library with marshmallow extension. Let’s first install the dependencies:
It is a regular web API, that parses the GET parameters, construct a SQLAlchemy query with pagination, and return the post list. The only thing special here is post_schema, which uses the marshmallow library to serialize post items. The schema is defined as:
1 2 3 4 5 6 7 8
classPostSchema(Schema): id = fields.Integer() title = fields.String(required=True, validate=validate.Length(min=1)) content = fields.String(required=True, validate=validate.Length(min=1)) updated_at = fields.DateTime(format='%Y-%m-%d %H:%M:%S', dump_only=True)
post_schema = PostSchema()
The schema is not that different from the SQLAlchemy model, but with extra information about validation, format, and whether some field should be dumped to the output. We will see how to use the schema to deserialize and validate the form data, later in save_post API.
1 2 3 4 5
classPost(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String) content = db.Column(db.String) updated_at = db.Column(db.DateTime)
The YAML is exactly the same as OpenAPI specification states, except the apispec library will use the route and schema to generate the complete spec file.
Generate specification file
To wire these routes and schemas, we create an openapi.py file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
from apispec import APISpec from apispec.ext.marshmallow import MarshmallowPlugin from apispec_webframeworks.flask import FlaskPlugin
with app.test_request_context(): spec.path(view=views.get_post_list)
And then a CLI command tool to generate the specification:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
from pathlib import Path
from oasis import app from oasis.openapi import spec
@app.cli.command() defgen(): """Generate OpenAPI specification.""" spec_path = Path(__file__).parent.joinpath('../openapi.yaml').resolve() withopen(spec_path, 'w') as f: f.write('# DO NOT EDIT\n') f.write('# Auto generated by "flask gen"\n') f.write('\n') f.write(spec.to_yaml())
post = db.session.query(Post).get(value) if post isNone: raise ValidationError('Post ID not found.')
Preview API docs
OpenAPI enjoys a nice documentation UI that you can preview in VS Code. Install the OpenAPI (Swagger) Editor extension, and turn on the preview alongside openapi.yaml:
If you want to run Try it out, make sure you enable the CORS of the API server. For Flask, there is an extension for this task:
1 2 3 4 5 6
from flask_cors import CORS
app = Flask(__name__)
if app.debug: CORS(app)
API Client
Now we have a complete OpenAPI specification file, we can generate all forms of clients and stubs from it. Here we use the OpenAPI Generator to create a TypeScript-Fetch client, and use it in a Vue project.
Generate client codes
First add the development tool via package manager like yarn:
1
yarn add -D @openapitools/openapi-generator-cli
And create a configuration file openapitools.json for OpenAPI generator:
downloadUrl can be used when you have trouble reaching the Maven central. generators section indicates the path of openapi.yaml, which generator we use, and the output path. For convenience, we create a script in package.json:
Note the base path of the API request is http://127.0.0.1:5000, as we defined in openapi.py, which should be overridden in production. Since typically the frontend code and API server are deployed under the same domain, we can set the base path to an empty string. Create an api.ts file:
1 2 3 4 5 6 7
import { Configuration, PostApi } from'./openapi'
const conf = newConfiguration({ basePath: '', })
exportconst postApi = newPostApi(conf)
And use it in your web application:
1 2 3
import { postApi } from'./api'
postApi.getPostList()
Display post list
Let’s request for the posts when page is loaded, and display them via Vue and Bootstrap: