How to generate an OpenAPI document with Flask
When building REST APIs with Flask, clear and accurate documentation helps API consumers integrate quickly and confidently. The OpenAPI Specification has become the industry standard for documenting RESTful APIs, but writing and maintaining OpenAPI documents by hand is tedious and error-prone.
The flask-smorest extension solves this by generating OpenAPI documentation directly from Flask code, using decorators and marshmallow schemas already in use. This guide covers generating an OpenAPI document from a Flask project and using it to create SDKs with Speakeasy, covering the following:
- Setting up a Flask REST API with
flask-smorest - Integrating OpenAPI document generation
- Generating and customizing the OpenAPI spec
- Using the Speakeasy CLI to generate client SDKs
What is flask-smorest?
flask-smorest is a Flask extension that generates OpenAPI documentation from application code. Unlike tools that parse docstrings or require separate spec files, flask-smorest uses decorators on view classes to describe endpoints, validate request data, and serialize responses — all while automatically building an accurate OpenAPI document.
flask-smorest builds on two Flask concepts:
Blueprint: flask-smorest’sBlueprintextends Flask’s built-in blueprint with OpenAPI integration. Blueprints are registered with theApiobject instead of the Flask app directly.MethodView: Flask’s class-based views groupget,post,put, anddeletemethods under a single route, making it easy to apply flask-smorest decorators consistently.
The two main decorators are:
@blp.response(status_code, schema): Specifies the response code and marshmallow schema for a method. flask-smorest uses this to document the response and serialize the return value.@blp.arguments(schema): Validates and deserializes the request body against a marshmallow schema, and documents the expected request body in the OpenAPI spec.
Sample API code
Example repository
The source code for the completed example is available in the Speakeasy examples repository (framework-flask).
The repository contains all the code covered throughout this guide. Clone it to follow along, or use it as a reference for an existing Flask project.
For this guide, we’ll use a simple Formula 1 (F1) lap times API with the following resources:
- Circuits: Racing circuits with names and locations
- Drivers: F1 drivers with their names and codes
- Lap Times: Records of lap times for drivers on specific circuits
Requirements
To follow this guide:
- Python 3.8 or higher
- An existing Flask project or a copy of the provided example repositoryÂ
- A basic understanding of Flask and REST APIs
Setting up flask-smorest
To follow along with the example repository, create and activate a virtual environment and install the project dependencies:
python -m venv venv
source venv/bin/activate
pip install -r requirements.txtTo add flask-smorest to an existing project, install it with:
pip install flask-smorestConfiguring flask-smorest
The core configuration for generating an OpenAPI document is added in app.py:
from flask import Flask
from flask_smorest import Api
from flask_migrate import Migrate
from db import db
import models
from resources import CircuitBlueprint, DriverBlueprint, LapTimeBlueprint
import yaml
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config["API_TITLE"] = "F1 Laps API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.1.0"
app.config["OPENAPI_URL_PREFIX"] = "/"
app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui"
app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database-file.db"
db.init_app(app)
migrate = Migrate(app, db)
api = Api(app)
api.register_blueprint(CircuitBlueprint)
api.register_blueprint(DriverBlueprint)
api.register_blueprint(LapTimeBlueprint)The API_TITLE, API_VERSION, and OPENAPI_VERSION keys become the info block in the generated OpenAPI document. The OPENAPI_SWAGGER_UI_PATH and OPENAPI_SWAGGER_UI_URL keys enable the built-in OpenAPI UI.
Adding a server and download endpoint
Server information and a downloadable spec endpoint can be added to app.py:
# Add server information to the OpenAPI spec
api.spec.options["servers"] = [
{
"url": "http://127.0.0.1:5000",
"description": "Local development server"
}
]
# Serve OpenAPI spec document endpoint for download
@app.route("/openapi.yaml")
def openapi_yaml():
spec = api.spec.to_dict()
return app.response_class(
yaml.dump(spec, default_flow_style=False),
mimetype="application/x-yaml"
)Writing the API
Models
The models.py file defines the SQLAlchemy models. A TimestampMixin adds a created_at field to all models:
from db import db
import datetime
class TimestampMixin(object):
created_at = db.Column(db.DateTime, default=datetime.datetime.now)
class Circuit(TimestampMixin, db.Model):
__tablename__ = "circuits"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
location = db.Column(db.String(80), nullable=False)
class Driver(TimestampMixin, db.Model):
__tablename__ = "drivers"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
code = db.Column(db.String(80), nullable=False)
class LapTime(TimestampMixin, db.Model):
__tablename__ = "lap_times"
id = db.Column(db.Integer, primary_key=True)
driver_id = db.Column(db.Integer, db.ForeignKey(Driver.id))
circuit_id = db.Column(db.Integer, db.ForeignKey(Circuit.id))
lap_number = db.Column(db.Integer)
time_ms = db.Column(db.Integer)Schemas
The schemas.py file defines marshmallow schemas for serialization and deserialization. flask-smorest uses these schemas to validate request data and generate OpenAPI schema components:
from marshmallow import Schema, fields
class CircuitSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
location = fields.Str(required=True)
created_at = fields.DateTime(dump_only=True)
class DriverSchema(Schema):
id = fields.Int(dump_only=True)
name = fields.Str(required=True)
code = fields.Str(required=True)
created_at = fields.DateTime(dump_only=True)
class LapTimeSchema(Schema):
id = fields.Int(dump_only=True)
driver_id = fields.Int(required=True)
circuit_id = fields.Int(required=True)
lap_number = fields.Int(required=True)
time_ms = fields.Int(required=True)
created_at = fields.DateTime(dump_only=True)Fields marked dump_only=True (such as id and created_at) appear in responses but are ignored in request bodies.
Resources
The resources.py file defines the API endpoints using flask-smorest Blueprints and MethodView. Here’s CircuitBlueprint as a representative example — DriverBlueprint and LapTimeBlueprint follow the same pattern:
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from sqlalchemy.exc import IntegrityError
from db import db
from models import Circuit, Driver, LapTime
from schemas import CircuitSchema, DriverSchema, LapTimeSchema
CircuitBlueprint = Blueprint("Circuits", "circuits", url_prefix="/circuits", description="Operations on circuits")
@CircuitBlueprint.route("/")
class CircuitList(MethodView):
@CircuitBlueprint.response(200, CircuitSchema(many=True))
def get(self):
"""List all circuits"""
return Circuit.query.all()
@CircuitBlueprint.arguments(CircuitSchema)
@CircuitBlueprint.response(201, CircuitSchema)
def post(self, new_data):
"""Create a new circuit"""
circuit = Circuit(**new_data)
db.session.add(circuit)
db.session.commit()
return circuit
@CircuitBlueprint.route("/<int:circuit_id>")
class CircuitDetail(MethodView):
@CircuitBlueprint.response(200, CircuitSchema)
def get(self, circuit_id):
"""Get circuit by ID"""
return Circuit.query.get_or_404(circuit_id)
@CircuitBlueprint.arguments(CircuitSchema)
@CircuitBlueprint.response(200, CircuitSchema)
def put(self, updated_data, circuit_id):
"""Update an existing circuit"""
circuit = Circuit.query.get_or_404(circuit_id)
circuit.name = updated_data["name"]
circuit.location = updated_data["location"]
db.session.commit()
return circuit
def delete(self, circuit_id):
"""Delete a circuit"""
circuit = Circuit.query.get_or_404(circuit_id)
db.session.delete(circuit)
db.session.commit()
return {"message": "Circuit deleted"}, 204Generating the OpenAPI document
Run the development server to create the database and serve the API:
python app.pyThe following endpoints are now available:
http://127.0.0.1:5000/openapi-ui— OpenAPI UI for interactive documentationhttp://127.0.0.1:5000/openapi.yaml— the downloadable OpenAPI document
To write a static openapi.yaml file to the project root, run:
flask openapi write --format=yaml openapi.yamlThe generated OpenAPI document
Here’s the OpenAPI document flask-smorest generates for the F1 Laps API:
components:
responses:
DEFAULT_ERROR:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
description: Default error response
UNPROCESSABLE_ENTITY:
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
description: Unprocessable Entity
schemas:
Circuit:
properties:
created_at:
format: date-time
readOnly: true
type: string
id:
readOnly: true
type: integer
location:
type: string
name:
type: string
required:
- name
- location
type: object
Driver:
properties:
code:
type: string
created_at:
format: date-time
readOnly: true
type: string
id:
readOnly: true
type: integer
name:
type: string
required:
- name
- code
type: object
Error:
properties:
code:
description: Error code
type: integer
errors:
additionalProperties: {}
description: Errors
type: object
message:
description: Error message
type: string
status:
description: Error name
type: string
type: object
LapTime:
properties:
circuit_id:
type: integer
created_at:
format: date-time
readOnly: true
type: string
driver_id:
type: integer
id:
readOnly: true
type: integer
lap_number:
type: integer
time_ms:
type: integer
required:
- circuit_id
- driver_id
- lap_number
- time_ms
type: object
info:
title: F1 Laps API
version: v1
openapi: 3.1.0
paths:
/circuits:
get:
responses:
'200':
content:
application/json:
schema:
items:
$ref: '#/components/schemas/Circuit'
type: array
description: OK
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: List all circuits
tags:
- Circuits
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Circuit'
required: true
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/Circuit'
description: Created
'422':
$ref: '#/components/responses/UNPROCESSABLE_ENTITY'
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Create a new circuit
tags:
- Circuits
/circuits{circuit_id}:
delete:
responses:
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Delete a circuit
tags:
- Circuits
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Circuit'
description: OK
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Get circuit by ID
tags:
- Circuits
parameters:
- in: path
name: circuit_id
required: true
schema:
minimum: 0
type: integer
put:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Circuit'
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Circuit'
description: OK
'422':
$ref: '#/components/responses/UNPROCESSABLE_ENTITY'
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Update an existing circuit
tags:
- Circuits
/drivers:
get:
responses:
'200':
content:
application/json:
schema:
items:
$ref: '#/components/schemas/Driver'
type: array
description: OK
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: List all drivers
tags:
- Drivers
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Driver'
required: true
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/Driver'
description: Created
'422':
$ref: '#/components/responses/UNPROCESSABLE_ENTITY'
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Create a new driver
tags:
- Drivers
/drivers/{driver_id}:
delete:
responses:
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Delete a driver
tags:
- Drivers
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Driver'
description: OK
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Get driver by ID
tags:
- Drivers
parameters:
- in: path
name: driver_id
required: true
schema:
minimum: 0
type: integer
put:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Driver'
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Driver'
description: OK
'422':
$ref: '#/components/responses/UNPROCESSABLE_ENTITY'
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Update an existing driver
tags:
- Drivers
/lap-times:
get:
responses:
'200':
content:
application/json:
schema:
items:
$ref: '#/components/schemas/LapTime'
type: array
description: OK
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: List all lap times
tags:
- LapTimes
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LapTime'
required: true
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/LapTime'
description: Created
'422':
$ref: '#/components/responses/UNPROCESSABLE_ENTITY'
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Create a new lap time
tags:
- LapTimes
/lap-times/{lap_time_id}:
delete:
responses:
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Delete a lap time
tags:
- LapTimes
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/LapTime'
description: OK
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Get lap time by ID
tags:
- LapTimes
parameters:
- in: path
name: lap_time_id
required: true
schema:
minimum: 0
type: integer
put:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LapTime'
required: true
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/LapTime'
description: OK
'422':
$ref: '#/components/responses/UNPROCESSABLE_ENTITY'
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: Update an existing lap time
tags:
- LapTimes
servers:
- description: Local development server
url: http://127.0.0.1:5000
tags:
- description: Operations on circuits
name: Circuits
- description: Operations on drivers
name: Drivers
- description: Operations on lap times
name: LapTimesAPI metadata
The app.py configuration keys map directly to the info block in the generated document:
app.config["API_TITLE"] = "F1 Laps API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.1.0"info:
title: F1 Laps API
version: v1
openapi: 3.1.0Server information
The server URL added in app.py appears in the servers block:
servers:
- description: Local development server
url: http://127.0.0.1:5000Model parameters
Open models.py to see the Circuit model fields:
class Circuit(TimestampMixin, db.Model):
__tablename__ = "circuits"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
location = db.Column(db.String(80), nullable=False)Schema in the OpenAPI document
Open openapi.yaml to see the same fields reflected in the Circuit schema:
schemas:
Circuit:
properties:
created_at:
format: date-time
readOnly: true
type: string
id:
readOnly: true
type: integer
location:
type: string
name:
type: string
required:
- name
- location
type: objectCustomizing the OpenAPI document
The OpenAPI document flask-smorest generates can be enriched with additional detail. Decorators on view methods control both API behavior and the resulting documentation.
Response schema in the OpenAPI document
Use @blp.response() to specify the expected response code and schema for a method:
@CircuitBlueprint.response(200, CircuitSchema(many=True))
def get(self):
"""List all circuits"""
return Circuit.query.all()This generates the following entry for the GET /circuits operation in the OpenAPI document:
/circuits:
get:
responses:
'200':
content:
application/json:
schema:
items:
$ref: '#/components/schemas/Circuit'
type: array
description: OK
default:
$ref: '#/components/responses/DEFAULT_ERROR'
summary: List all circuits
tags:
- CircuitsRequest body in the OpenAPI document
Use @blp.arguments() to validate the request body against a schema and document it in the spec:
@CircuitBlueprint.arguments(CircuitSchema)
@CircuitBlueprint.response(201, CircuitSchema)
def post(self, new_data):
"""Create a new circuit"""
...This adds a requestBody to the POST operation:
post:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Circuit'
required: trueDocstrings in the OpenAPI document
Method docstrings become the summary field in the OpenAPI document. Anything after --- in the docstring is treated as an internal comment and omitted from the spec:
def get(self, circuit_id):
"""Get circuit by ID.
---
Internal note: add caching here later.
"""
return Circuit.query.get_or_404(circuit_id)This produces:
summary: Get circuit by ID.The internal comment is excluded from the generated document.
Retries in the generated SDK
Global retry behavior for Speakeasy-generated SDKs can be configured by adding an x-speakeasy-retries extension to the OpenAPI spec via app.config["API_SPEC_OPTIONS"]:
app.config["API_SPEC_OPTIONS"] = {"x-speakeasy-retries": {
'strategy': 'backoff',
'backoff': {
'initialInterval': 500,
'maxInterval': 60000,
'maxElapsedTime': 3600000,
'exponent': 1.5,
},
'statusCodes': ['5XX'],
'retryConnectionErrors': True,
}
}This adds the following to the generated OpenAPI document, which Speakeasy uses when generating SDK retry logic:
x-speakeasy-retries:
backoff:
exponent: 1.5
initialInterval: 500
maxElapsedTime: 3600000
maxInterval: 60000
retryConnectionErrors: true
statusCodes:
- 5XX
strategy: backoffGenerating SDKs with Speakeasy
With the OpenAPI document ready, Speakeasy can generate client SDKs. First, install the Speakeasy CLI:
curl -fsSL https://go.speakeasy.com/cli-install.sh | shThen run:
speakeasy quickstartFollow the prompts to provide the OpenAPI document location (openapi.yaml), choose a language, and generate. Speakeasy produces a complete client SDK based on your API specification.
Add SDK generation to GitHub Actions
The Speakeasy sdk-generation-action repository provides workflows for integrating the Speakeasy CLI into a CI/CD pipeline, so SDKs are regenerated whenever the OpenAPI document changes.
For an overview of how to set up SDK automation, see the Speakeasy SDK Generation Action and Workflows documentation.
Summary
This guide covered how to generate an OpenAPI document for a Flask API using flask-smorest and use Speakeasy to create client SDKs from it. We covered setting up flask-smorest, writing models, schemas, and blueprint-based resources for the F1 Laps API, generating and inspecting the OpenAPI document, and customizing it with response schemas, request validation, docstrings, and retry configuration.
Last updated on