# Generate an OpenAPI/Swagger document with Flask

# 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](https://spec.openapis.org/oas/latest.html) 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](https://flask-smorest.readthedocs.io/) 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's `Blueprint` extends Flask's built-in blueprint with OpenAPI integration. Blueprints are registered with the `Api` object instead of the Flask app directly.
- **`MethodView`**: Flask's class-based views group `get`, `post`, `put`, and `delete` methods 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

<Callout title="Example repository" type="info">
The source code for the completed example is available in the [**Speakeasy examples repository**](https://github.com/speakeasy-api/examples.git) (framework-flask).
</Callout>

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](https://github.com/speakeasy-api/examples/tree/main/framework-flask)
- 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:

```bash filename="Terminal"
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```

To add flask-smorest to an existing project, install it with:

```bash filename="Terminal"
pip install flask-smorest
```

### Configuring flask-smorest

The core configuration for generating an OpenAPI document is added in `app.py`:

```python filename="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`:

```python filename="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:

```python filename="models.py"
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:

```python filename="schemas.py"
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:

```python filename="resources.py"
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"}, 204
```

## Generating the OpenAPI document

Run the development server to create the database and serve the API:

```bash filename="Terminal"
python app.py
```

The following endpoints are now available:

- `http://127.0.0.1:5000/openapi-ui` — OpenAPI UI for interactive documentation
- `http://127.0.0.1:5000/openapi.yaml` — the downloadable OpenAPI document

To write a static `openapi.yaml` file to the project root, run:

```bash filename="Terminal"
flask openapi write --format=yaml openapi.yaml
```

### The generated OpenAPI document

Here's the OpenAPI document flask-smorest generates for the F1 Laps API:

```yaml filename="openapi.yaml"
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: LapTimes
```

### API metadata

The `app.py` configuration keys map directly to the `info` block in the generated document:

```python filename="app.py"
app.config["API_TITLE"] = "F1 Laps API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.1.0"
```

```yaml filename="openapi.yaml"
info:
  title: F1 Laps API
  version: v1
openapi: 3.1.0
```

### Server information

The server URL added in `app.py` appears in the `servers` block:

```yaml filename="openapi.yaml"
servers:
- description: Local development server
  url: http://127.0.0.1:5000
```

### Model parameters

Open `models.py` to see the `Circuit` model fields:

```python filename="models.py"
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:

```yaml filename="openapi.yaml"
  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
```

## Customizing 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:

```python filename="resources.py"
    @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:

```yaml filename="openapi.yaml"
  /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
```

### Request body in the OpenAPI document

Use `@blp.arguments()` to validate the request body against a schema and document it in the spec:

```python filename="resources.py"
    @CircuitBlueprint.arguments(CircuitSchema)
    @CircuitBlueprint.response(201, CircuitSchema)
    def post(self, new_data):
        """Create a new circuit"""
        ...
```

This adds a `requestBody` to the `POST` operation:

```yaml filename="openapi.yaml"
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Circuit'
        required: true
```

### Docstrings 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:

```python filename="resources.py"
    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:

```yaml filename="openapi.yaml"
      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"]`:

```python filename="app.py"
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:

```yaml filename="openapi.yaml"
x-speakeasy-retries:
  backoff:
    exponent: 1.5
    initialInterval: 500
    maxElapsedTime: 3600000
    maxInterval: 60000
  retryConnectionErrors: true
  statusCodes:
  - 5XX
  strategy: backoff
```

## Generating SDKs with Speakeasy

With the OpenAPI document ready, Speakeasy can generate client SDKs. First, install the Speakeasy CLI:

```bash filename="Terminal"
curl -fsSL https://go.speakeasy.com/cli-install.sh | sh
```

Then run:

```bash filename="Terminal"
speakeasy quickstart
```

Follow 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`](https://github.com/speakeasy-api/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](/docs/speakeasy-reference/workflow-file) 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.
