Introduction: Your Flask API Has No "Contract"
Picture handing a hand-written Flask REST API to another team (front end, mobile, an external partner). The first thing they ask is always this: "What's this endpoint's request body? What shape is the response? What comes back on an error?"
And where is the answer? In many Flask projects, the answer is reading the view-function code, asking on Slack, or sharing a Postman collection — all "substance-less documentation" maintained by hand, separate from the code. The code changes but the docs don't. Eventually the two diverge, and the docs become "lying documentation." This is technical debt itself.
The essence of the problem is that a hand-written Flask API has no machine-readable contract. Even if you carefully do Flask REST API design (MethodView / Blueprint / versioning), that design exists only in code humans read, not in a spec (OpenAPI doc) the API's consumers can reference mechanically.
Here, many recall FastAPI. As touched on in Flask vs FastAPI vs Django, FastAPI generates the OpenAPI spec and Swagger UI "for free" from type hints. This is FastAPI's decisive appeal, and the representative example of what Flask is considered "not to have."
This article's claim is simple. That gap is closed if you use Flask-smorest on the Flask side. Staying WSGI / Flask, auto-generate the OpenAPI spec, Swagger UI, and ReDoc from a single marshmallow schema, with that same schema also handling input validation and response shaping — what FastAPI gets with types, Flask gets with a marshmallow schema.
The author designed, implemented, and ran in production the backend of a B2B SaaS that won the Minister of Economy, Trade and Industry Award in Python / Flask / SQLAlchemy / PostgreSQL. Providing a "machine-readable contract" to the in-house front-end team, and to external partners hitting the API, supported both development speed and trust. This article systematizes the "documentation automation" design that was needed in that field experience, with real code faithful to the Flask-smorest official documentation.
💡 The version covered in this article: it assumes Flask-smorest 0.47.0. Flask-smorest is "a REST API framework based on Flask / Marshmallow," with dependencies flask>=3.0.2,<4 / marshmallow>=3.24.1,<5 / webargs>=8 / apispec[marshmallow]>=6, requiring Python 3.10 or above. This article's code is based on the official documentation's patterns. The design of marshmallow schemas themselves is treated as prerequisite knowledge in the marshmallow practical guide.
1. What Is Flask-smorest: A "Schema-Driven Core" Bundling 4 Libraries
Flask-smorest is not an API framework rebuilt from scratch. It's a thin adhesive layer that bundles 4 already-mature libraries and "drives them with a single schema."
| Component | Role | Its positioning in smorest |
|---|---|---|
| Flask | WSGI application / routing | The foundation. Loaded as an extension with Api(app) |
| marshmallow | Validation / serialization via schemas | The contract's single source of truth (input, output, and spec) |
| webargs | Request parsing (query/json/path…) | The substance of @blp.arguments |
| apispec | Converting marshmallow schemas → the OpenAPI spec | The engine of doc auto-generation |
What this composition means is that a single marshmallow schema does 3 jobs at once.
- Validating the input boundary (
loadvia webargs): validate the JSON / query that came from the client, and auto-return 422 if invalid - Shaping the output boundary (
dump): shape the return value with the schema, preventing the leakage of internal attributes - Generating the OpenAPI spec (via apispec): generate the request-body / response JSON Schema from the same schema and display it in Swagger UI
In the marshmallow × Flask × SQLAlchemy guide, I argued that "a single schema protects the 2 boundaries of entrance and exit." Flask-smorest adds a 3rd boundary there — machine-readable documentation — at zero additional cost. This is the apex of DRY. Rewrite the schema, and validation, shaping, and documentation follow simultaneously, so there's no gap to diverge in the first place.
💡 A shift in thinking: FastAPI makes "Pydantic type hints" the single source of truth to generate OpenAPI. Flask-smorest makes "the marshmallow schema" the single source of truth to do the same thing. Only the means (type hints vs schema objects) differs; the value obtained is isomorphic. For a Flask project that has already invested in marshmallow, you can divert that investment to OpenAPI docs without migrating to ASGI.
2. Reading the Quickstart Closely: 4 New Concepts
The official quickstart is short, but the elements for understanding Flask-smorest are condensed into it. First look at the whole, then dissect it line by line.
from flask import Flask
from flask.views import MethodView
import marshmallow as ma
from flask_smorest import Api, Blueprint, abort
from .model import Pet
app = Flask(__name__)
app.config["API_TITLE"] = "My API"
app.config["API_VERSION"] = "v1"
app.config["OPENAPI_VERSION"] = "3.0.2"
api = Api(app)
class PetSchema(ma.Schema):
id = ma.fields.Int(dump_only=True)
name = ma.fields.String()
class PetQueryArgsSchema(ma.Schema):
name = ma.fields.String()
blp = Blueprint("pets", "pets", url_prefix="/pets", description="Operations on pets")
@blp.route("/")
class Pets(MethodView):
@blp.arguments(PetQueryArgsSchema, location="query")
@blp.response(200, PetSchema(many=True))
def get(self, args):
"""List pets"""
return Pet.get(filters=args)
@blp.arguments(PetSchema)
@blp.response(201, PetSchema)
def post(self, new_data):
"""Add a new pet"""
item = Pet.create(**new_data)
return item
api.register_blueprint(blp)
Here, 4 concepts specific to Flask-smorest appear. Let me look at them in order.
2.1 Api(app): Loading It as an Extension
With api = Api(app), you attach Flask-smorest to the app as an extension. This Api object is the command tower that holds apispec internally, scans the registered Blueprints to assemble the OpenAPI spec, and serves the Swagger UI / ReDoc endpoints. To combine with an application factory, you can use api.init_app(app) per the init_app pattern covered in the Flask production-operations guide.
⚠️ The 3 of
API_TITLE/API_VERSION/OPENAPI_VERSIONare mandatory. Without them,Api(app)fails at startup.API_VERSION(your API's version, e.g.v1) andOPENAPI_VERSION(the version of the OpenAPI spec itself, e.g.3.0.2) are different things, so don't conflate them.
2.2 smorest's Blueprint: Not Flask's Standard Blueprint
The first pitfall is here.
from flask_smorest import Api, Blueprint, abort
This Blueprint is not flask.Blueprint but Flask-smorest's own Blueprint. It inherits Flask's standard Blueprint and additionally has OpenAPI-aware decorators (@blp.arguments / @blp.response / @blp.paginate …). Its constructor takes description=, and this becomes the OpenAPI tag description.
Similarly, abort is not flask.abort but smorest's enhanced abort. You can put an error message and additional info into the JSON error response (detailed in §5).
⚠️ Anti-pattern: mixing
from flask import Blueprint, abortandfrom flask_smorest import Blueprint, abort. Use the former and@blp.argumentsdoesn't exist, giving anAttributeError. In files using Flask-smorest, import Blueprint / abort always fromflask_smorest.
2.3 @blp.route("/") Decorates a Class
@blp.route("/")
class Pets(MethodView):
def get(self, args): ...
def post(self, new_data): ...
Flask-smorest treats class-based views (MethodView) as first-class. @blp.route("/") decorates the whole class, and the get / post methods within the class map to HTTP GET / POST respectively. MethodView is a Flask-core class (flask.views.MethodView), not smorest's own.
Because you can bundle multiple HTTP methods for one URL into one class, it naturally meshes with resource-oriented REST design. The design philosophy of MethodView itself (resource = class, method = HTTP verb) is covered in detail in the Flask REST API design guide. This article layers "documentation automation" onto that design.
2.4 The Order of Decorators and the Docstring
Notice the 2 decorators on the method, and the docstring below them.
@blp.arguments(PetSchema) # 入力:検証して引数に注入
@blp.response(201, PetSchema) # 出力:シリアライズしてステータス設定
def post(self, new_data):
"""Add a new pet""" # ← OpenAPI の summary になる
item = Pet.create(**new_data)
return item
@blp.arguments(PetSchema): validate the request body withPetSchema, and inject the validated dict as thenew_dataargument (§3)@blp.response(201, PetSchema): serialize the return valueitemwithPetSchema, and set HTTP 201 (§4)"""Add a new pet""": the docstring becomes the OpenAPI operation summary. The source of the description shown in Swagger UI
With these 3 in place, POST /pets is automatically loaded into the docs as a machine-readable spec: "the request body is PetSchema, on success returns 201 with PetSchema, the description is Add a new pet." That writing the view itself is writing the documentation is the core of Flask-smorest.
3. @blp.arguments: Validating and Injecting the Input Boundary
@blp.arguments is the protagonist of Flask-smorest's entrance side. It does 2 things.
- Take the request data from the specified location and validate (
load) it with the schema - Inject the validated data into the view function as an argument
If validation fails, Flask-smorest auto-returns 422 Unprocessable Entity. You don't need to write try/except ValidationError inside the view function — boundary validation is declaratively externalized.
3.1 location: Where to Read From
The location of @blp.arguments(Schema, location=...) specifies where the data is taken from.
| location | Source | Use |
|---|---|---|
json (default) | The request body's JSON | POST / PUT payloads |
query | The query string | List filtering / search parameters |
path | URL path parameters | Resource IDs |
form | Form data | HTML form submissions |
headers | Request headers | Validating custom headers |
cookies | Cookies | — |
files | Uploaded files | Multipart |
json_or_form | JSON or form | Endpoints supporting both |
Omit location and json is the default. When you want to validate query parameters, explicitly state location="query", like the quickstart's GET.
3.2 How It's Injected: Positional Args / Keyword Args / Stacking
By default, @blp.arguments injects the validated data as a single positional argument (a dict).
@blp.arguments(PetSchema)
def post(self, new_data): # new_data は検証済み dict
...
Pass as_kwargs=True and it's injected expanded as **kwargs rather than a dict.
@blp.arguments(PetSchema, as_kwargs=True)
def post(self, name, **kwargs): # スキーマのフィールドが個別のキーワード引数に
...
Stacking multiple @blp.arguments injects multiple positional arguments in declaration order. This is effective when you want to validate query and json simultaneously.
@blp.arguments(PetQueryArgsSchema, location="query")
@blp.arguments(PetSchema, location="json")
def post(self, query_args, body):
# query_args = query から、body = JSON から(装飾子の順に対応)
...
💡 The argument order of stacking corresponds to the decorator order "top to bottom." For readability, when the locations differ, give variable names that make the source clear, like
query_args/body, so a later reader doesn't get lost.
3.3 422 Is "Auto-Documented"
This is the decisive difference from a hand-written API. An endpoint with @blp.arguments has a validation-error (422) response automatically added to the OpenAPI spec. That is, the API's consumer can know mechanically, just by looking at Swagger UI, that "send invalid input and 422 comes back, and the error's shape is this." Hand-written, the existence and shape of this 422 was "tacit knowledge you can't know without reading the code." Flask-smorest turns it into an explicit contract.
4. @blp.response: Shaping the Output Boundary and Setting the Status
@blp.response(status_code, Schema) is the protagonist of the exit side. It does 3 things.
dump(serialize) the view function's return value with the schema- Set the HTTP status code
- Register that response (status + schema) in the OpenAPI spec
@blp.response(200, PetSchema(many=True))
def get(self, args):
return Pet.get(filters=args) # ORMオブジェクトのリスト → PetSchema で整形
When returning a list, use Schema(many=True). many=True is reflected in the OpenAPI spec too, correctly documenting "the response is an array."
The power of dump_only fields (e.g. id = ma.fields.Int(dump_only=True)) also works here. id is not accepted on input (@blp.arguments) — preventing mass assignment — and is returned on output (@blp.response); you can declare a "read-only attribute" with one schema. This input/output-asymmetric design is as detailed in the marshmallow serialization/validation guide, and smorest translates it directly to OpenAPI's readOnly.
⚠️ An important exception (official caution): if the view function returns a
werkzeug.BaseResponse(= aResponseobject or the result ofmake_response()), that Response is returned as-is, and neither schemadumpnor status-code application happens. For cases where you build a Response directly, like a file download or a redirect, understand that@blp.response's schema shaping doesn't take effect. If you want schema shaping, the iron rule is to return a dict or an ORM object (don't make a Response).
5. Pagination and Errors: Turning Lists and Abnormal Cases into "Documented Contracts"
What most tends to cause "spec divergence" in a practical REST API is lists (pagination) and abnormal cases (errors). Flask-smorest has mechanisms to fix both as contracts.
5.1 @blp.paginate(): Declaring Pagination
@blp.route("/")
class Pets(MethodView):
@blp.response(200, PetSchema(many=True))
@blp.paginate()
def get(self, pagination_parameters):
pagination_parameters.item_count = Pet.size
return Pet.get_elements(
first_item=pagination_parameters.first_item,
last_item=pagination_parameters.last_item,
)
@blp.paginate() injects a PaginationParameters object into the view. What you get from it, and what you should do, is as follows.
- Injected:
pagination_parameters(.page/.page_size, the computed.first_item/.last_item) - You set:
pagination_parameters.item_count = <total count>(needed to compute the total page count) - Auto-attached: the pagination metadata rides in the
X-Paginationresponse header
The default parameters are DEFAULT_PAGINATION_PARAMETERS = {"page": 1, "page_size": 10, "max_page_size": 100}. The page / page_size query parameters and their defaults / upper bound (max_page_size) are also auto-documented in the OpenAPI spec. The client, just by looking at Swagger UI, knows mechanically "hit it with ?page=2&page_size=50, with an upper bound of 100."
💡 Putting pagination info in the
X-Paginationheader rather than the body is a design judgment. The response body is purified to "an array of resources," and the metadata (total count, whether there's a next page) is separated into the header. It's easier for the client to parse the metadata, and the body's schema isn't polluted by meta.
5.2 abort(): Documented Consistent Errors
Return abnormal cases with smorest's abort.
from flask_smorest import abort
@blp.route("/<int:pet_id>")
class PetById(MethodView):
@blp.response(200, PetSchema)
def get(self, pet_id):
pet = Pet.get_by_id(pet_id)
if pet is None:
abort(404, message="Pet not found")
return pet
smorest's abort extends flask.abort and can include additional info like message in the JSON error response. Flask-smorest shapes the error response into a consistent form (code / status / message / errors on validation errors), and moreover registers that form in the OpenAPI spec. That is, "this endpoint can return 404, and the error's shape is this" rides in the contract.
This "documented consistent error envelope," done by hand, will surely sprawl. The "unified JSON errors across all endpoints" argued in the Flask error-handling / observability guide, smorest provides as standard, and documented to boot.
| Abnormal case | Hand-written Flask | Flask-smorest |
|---|---|---|
| Validation error (422) | Hand-write try/except everywhere | @blp.arguments auto 422 + documentation |
| Not Found (404) | Scatter jsonify(...), 404 | Consistent + documented with abort(404, message=...) |
| Error shape | Different per endpoint | Unified envelope + registered in OpenAPI |
| Conveying to consumers | Read the code / verbally | Machine-readable in Swagger UI |
6. OpenAPI Settings and UI Serving: Enabling Swagger UI / ReDoc
So far you've understood that "the spec is auto-generated." Next is where and how to serve that spec. Flask-smorest can serve the OpenAPI JSON, Swagger UI, and ReDoc with config alone.
# OpenAPI 配信の設定(公式の既定値つき)
OPENAPI_VERSION = "3.0.2" # 必須
OPENAPI_URL_PREFIX = "/" # 既定 None → 設定しないと仕様を配信しない
OPENAPI_JSON_PATH = "openapi.json" # 既定。OpenAPI JSON の配信パス
OPENAPI_SWAGGER_UI_PATH = "/swagger-ui" # 既定 None → 設定すると Swagger UI 有効
OPENAPI_SWAGGER_UI_URL = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/"
OPENAPI_REDOC_PATH = "/redoc" # 既定 None → 設定すると ReDoc 有効
OPENAPI_REDOC_URL = "https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"
There are 3 default-value behaviors to pin down.
OPENAPI_URL_PREFIX's default isNone. Unless you set this, the OpenAPI spec itself isn't served. Only after setting it to"/"or the like does/openapi.jsonbecome visible.OPENAPI_SWAGGER_UI_PATH's default is alsoNone. Only after setting it does Swagger UI become enabled. In the example above, you can see the interactive UI at/swagger-ui.OPENAPI_REDOC_PATH's default is alsoNone. Only after setting it does ReDoc become enabled. ReDoc is read-only, easy-to-read three-column documentation.
💡 When to use Swagger UI vs ReDoc: Swagger UI is "hittable" (with Try it out you can send real requests from the browser), so it's powerful in the development / verification phase. ReDoc is "readable" (beautiful static docs), so it suits external publication / reference use. The standard is to enable both and use them as appropriate. The default configuration loads the UI's JS assets from a CDN (jsDelivr).
7. Production-Operation Manners: Protection, Versioning, Code Generation in CI
There's a chasm between "works" and "production quality." Let me list the points I always pin down when shipping Flask-smorest to production.
7.1 How to Handle Swagger UI in Production
Swagger UI, handy in development, should sometimes not be exposed as-is in production. There are 2 reasons. (a) Fully exposing the internal API's endpoints, schemas, and parameters hands over a map of the attack surface; (b) with "Try it out," destructive operations against production data can be tried. The countermeasures can be chosen in stages.
| Strategy | Method | Suited case |
|---|---|---|
| Full disabling | Don't set OPENAPI_SWAGGER_UI_PATH in the production config (None) | A fully internal API, no UI needed |
| Auth gate | Place the UI-serving path behind Basic auth or an IP restriction via a reverse proxy / extension | Want it viewable only internally |
| Serve JSON only | Serve openapi.json but not the UI | Consumers read it with their own tools |
| Full publication | Publish including the UI | Public APIs, partner portals |
The crux is that the config switches per environment. Per the "12-factor-izing of config" covered in the Flask production-operations guide, inject OPENAPI_SWAGGER_UI_PATH from environment variables / instance config, and switch like disabled in production, enabled in development. Don't write a fixed value in the code.
⚠️ "Exposing Swagger UI in production = an instant security incident" is not true. For an auth-required API, Try it out also requires auth, so it's not unconditionally dangerous. That said, the cost of "handing out a map of the attack surface for free" is always there, so unless it's a public API, make an auth gate or disabling the default — that's the safe-side judgment.
7.2 Versioning: url_prefix or Multiple Apis
API versioning can be expressed straightforwardly with smorest's Blueprint url_prefix.
v1 = Blueprint("orders_v1", "orders_v1", url_prefix="/api/v1/orders", description="Orders API v1")
v2 = Blueprint("orders_v2", "orders_v2", url_prefix="/api/v2/orders", description="Orders API v2")
api.register_blueprint(v1)
api.register_blueprint(v2)
Register multiple-version Blueprints to the same Api, and v1 / v2 line up in one OpenAPI spec. If you want to completely separate the specs per version, you can also take the configuration of standing up multiple Api instances and serving each at a different OPENAPI_URL_PREFIX. The design judgment of URL-path versioning (path vs header vs media type) I leave to the Flask REST API design guide.
7.3 Statically Generating openapi.json in CI to Connect to Client Type Safety
This is where Flask-smorest's investment pays off the most. The OpenAPI spec shouldn't end as just "documentation humans read" — that's a waste. You can make it the input for machines to read and auto-generate a typed client.
In CI, write the OpenAPI spec out to a static file. You can get the spec from Flask-smorest's Api.
# scripts/dump_openapi.py — CIで実行し openapi.json を成果物にする
import json
from myapp import create_app
def main() -> None:
app = create_app()
api = app.extensions["flask-smorest"]["apis"][""]["ext_obj"]
spec = api.spec.to_dict()
with open("openapi.json", "w", encoding="utf-8") as f:
json.dump(spec, f, ensure_ascii=False, indent=2, sort_keys=True)
if __name__ == "__main__":
main()
# CI(GitHub Actions 等)でのフロー例
python scripts/dump_openapi.py # サーバから仕様を抽出
npx @openapitools/openapi-generator-cli generate \
-i openapi.json -g typescript-fetch -o ./generated-client
# 生成された型付きクライアントをフロント / モバイルが import
Once this flow is complete, a type-safe pipeline forms: a backend schema change → openapi.json updated in CI → client types regenerated → breaking changes detected immediately as compile errors. The contract between server and client is guaranteed by the type system, not by human attentiveness. The whole picture of this "end-to-end type safety centered on OpenAPI" is argued in detail in the Next.js × Go end-to-end type-safety guide. Even if the backend is Flask, you can enjoy the same type-safety benefit by going through OpenAPI.
💡 The hygiene of tags / operationId: an auto-generated client's method names derive from OpenAPI's
operationId. Give each Blueprint a meaningful name anddescription, and write a clear docstring per endpoint, and the generated client's code becomes readable. Be conscious that "the quality of the docs = the quality of the client code."
8. Technology Selection: An Honest Comparison of smorest / APIFlask / Hand-Written / FastAPI
Flask-smorest isn't the only right answer. There are multiple means to obtain OpenAPI docs, each with its right place. Following the principle that technology selection is a matter of "fit," not "superiority", let me show an honest comparison.
8.1 The Option of APIFlask
A strong alternative to Flask-smorest is APIFlask 3.1.1. It's "a lightweight Flask-based Web API framework," and like smorest auto-generates the OpenAPI spec, Swagger UI, and ReDoc. The biggest difference is that its schema adapter is swappable, handling both marshmallow schemas and Pydantic models (Pydantic support is a relatively new feature added in the 3.x line).
Flask-smorest : marshmallow 一択(marshmallow に最適化)
APIFlask : marshmallow / Pydantic を選べる(pluggable schema adapter)
If you've already invested in Pydantic, or want to hold your schema assets in Pydantic looking ahead to a future FastAPI migration — in such cases APIFlask's Pydantic support works. Conversely, if you've already solidified your boundary design with marshmallow (many readers of this cluster have), the marshmallow-native smorest is straightforward.
8.2 A Decision Table: When to Choose What
| Option | OpenAPI auto-generation | Schema | Runtime | When to choose |
|---|---|---|---|---|
| Hand-written Flask | None (write it yourself) | Apply marshmallow etc. by hand | WSGI | An extremely small API of 1–2 endpoints. No docs needed |
| Flask-smorest | Yes (automatic) | marshmallow | WSGI | Want a contract while leveraging existing Flask + marshmallow assets |
| APIFlask | Yes (automatic) | marshmallow or Pydantic | WSGI | Want to use Pydantic while staying Flask / looking ahead to migration |
| FastAPI | Yes (standard) | Pydantic (type hints) | ASGI | New, high-concurrency IO, type-hint-driven, want to use async fully |
The decision can be organized thus.
- A new project that can go to ASGI, where you want to maximize type-hint-driven async → FastAPI. OpenAPI comes standard.
- You can't move off existing Flask (WSGI), or don't want to → Flask-smorest / APIFlask. Get OpenAPI while staying WSGI.
- Within that, smorest if you have marshmallow assets, APIFlask if you want to write in Pydantic.
- An extremely small internal API needing no contract in the first place → hand-written. But beware the premise that "no contract needed" crumbles over time. The moment endpoints exceed 3 and consumers become a separate team, the value of bringing in smorest stands up.
💡 "Flask, so OpenAPI is impossible" is a misunderstanding. FastAPI's OpenAPI auto-generation is certainly powerful, but it's not something "obtainable only with FastAPI." Flask-smorest / APIFlask provide equivalent value while staying WSGI. Weigh the cost of a framework migration (WSGI → ASGI) and the value of documentation automation on separate scales. "Migrate to FastAPI because I want OpenAPI" is, in many cases, overkill.
9. A Worked Example: A Documented /api/v1/orders Resource
Let me land the theory in a form actually needed in a B2B SaaS. I assemble listing and creating orders as a single resource complete with validation, shaping, pagination, errors, and documentation.
9.1 The Schema: The Contract's Single Source of Truth
# schemas.py
import marshmallow as ma
class OrderSchema(ma.Schema):
"""受注リソース。dump_only で読み取り専用、required で必須を宣言する。"""
id = ma.fields.Int(dump_only=True)
order_number = ma.fields.String(dump_only=True)
customer_id = ma.fields.Int(required=True)
amount = ma.fields.Decimal(required=True, as_string=True, validate=ma.validate.Range(min=0))
status = ma.fields.String(
dump_only=True,
validate=ma.validate.OneOf(["pending", "confirmed", "shipped", "cancelled"]),
)
created_at = ma.fields.DateTime(dump_only=True)
class OrderQueryArgsSchema(ma.Schema):
"""一覧の絞り込み条件。query から読む。"""
customer_id = ma.fields.Int()
status = ma.fields.String(validate=ma.validate.OneOf(["pending", "confirmed", "shipped", "cancelled"]))
id / order_number / status / created_at are dump_only — read-only attributes the server numbers / manages, which the client can't input (preventing mass assignment). customer_id / amount are required, auto-422 if missing. This single schema simultaneously drives input validation, output shaping, and the OpenAPI spec.
9.2 The Resource: View = Documentation
# views.py
from flask.views import MethodView
from flask_smorest import Blueprint, abort
from .schemas import OrderSchema, OrderQueryArgsSchema
from .service import OrderService
blp = Blueprint(
"orders",
"orders",
url_prefix="/api/v1/orders",
description="受注の一覧取得・作成を行う API",
)
@blp.route("/")
class Orders(MethodView):
@blp.arguments(OrderQueryArgsSchema, location="query")
@blp.response(200, OrderSchema(many=True))
@blp.paginate()
def get(self, filters, pagination_parameters):
"""受注を一覧する(顧客・ステータスで絞り込み可、ページネーション対応)"""
total, items = OrderService.list(
filters=filters,
first_item=pagination_parameters.first_item,
last_item=pagination_parameters.last_item,
)
pagination_parameters.item_count = total
return items
@blp.arguments(OrderSchema)
@blp.response(201, OrderSchema)
def post(self, new_order):
"""受注を作成する"""
if not OrderService.customer_exists(new_order["customer_id"]):
abort(422, message="customer_id が存在しません")
return OrderService.create(new_order)
Read the contract this code generates.
GET /api/v1/orders: filterable in query bycustomer_id/status, pageable withpage/page_size(default 10, upper bound 100), returns an array of Order with 200, total count in theX-PaginationheaderPOST /api/v1/orders: the body is OrderSchema (customer_id/amountrequired,idetc. not accepted), returns Order with 201 on success, 422 on validation failure, 422 on a business error (with message)
All of these are automatically loaded into Swagger UI. The front-end team can "Try it out" in the UI to try creating an order, an external partner reads the spec in ReDoc, and CI generates a typed client from openapi.json. In my B2B SaaS, being able to provide this "hittable contract" accelerated everything — the in-house front-end implementation, partner integration, and sales' technical explanations. The effort to write documentation separately is zero — because writing the schema and the view becomes the contract itself.
💡 Notice that business logic is offloaded to the Service layer (
OrderService), and the view sticks to a thin conversion of "HTTP ↔ schema ↔ service." This is the same thought as theRouter → Schema → Modellayered separation of the marshmallow × Flask × SQLAlchemy guide. smorest merely layers "documentation automation" onto the Router layer; it doesn't break the layers' separation of responsibilities.
10. Summary: Write a Schema, and You Get a Contract
The essence of Flask-smorest is integrating "writing views and schemas" and "maintaining a machine-readable contract" into the same single act. The OpenAPI docs FastAPI gets with type hints, Flask can get with a marshmallow schema, while staying WSGI. The era of "Flask, so docs are hand-written" is over.
The key points, one more time at the end.
- A hand-written Flask API has no machine-readable contract. That produces diverging docs, verbal spec-conveyance, and missed breaking changes
- Flask-smorest is a bundle of Flask + marshmallow + webargs + apispec. A single schema simultaneously drives input validation, output shaping, and the OpenAPI spec (the contract's single source of truth)
- With
@blp.arguments/@blp.response/@blp.paginate/abort, you can write validation, shaping, paging, and errors declaratively, and all of it is auto-documented - In production, protect Swagger UI per environment, and statically generate openapi.json in CI to connect to client type safety
- Choose the means from smorest / APIFlask / hand-written / FastAPI, by runtime (WSGI/ASGI) and schema assets (marshmallow/Pydantic)
An OpenAPI / Documentation-Automation Checklist
| # | Item | What to confirm |
|---|---|---|
| 1 | Mandatory config | Did you set API_TITLE / API_VERSION / OPENAPI_VERSION? |
| 2 | Import source | Are you importing Blueprint / abort from flask_smorest (not from flask)? |
| 3 | Input boundary | Did you attach @blp.arguments to all inputs and state location explicitly? |
| 4 | Output boundary | @blp.response(code, Schema) on all responses, many=True for lists |
| 5 | Read-only | Did you make server-numbered attributes dump_only and prevent mass assignment? |
| 6 | The Response-return trap | Do you understand that returning a Response skips dump, and return dict/ORM where you want shaping? |
| 7 | Pagination | @blp.paginate() on lists, set item_count, documented X-Pagination? |
| 8 | Errors | Consistent errors with abort(code, message=...), are 422/404 in the spec? |
| 9 | Serving the spec | Did you set OPENAPI_URL_PREFIX (with default None it isn't served)? |
| 10 | Enabling the UI | Did you set OPENAPI_SWAGGER_UI_PATH / OPENAPI_REDOC_PATH by use? |
| 11 | Production protection | Did you disable / auth-gate Swagger UI in production (switching per environment)? |
| 12 | Versioning | Did you express versions with url_prefix or multiple Apis? |
| 13 | Docstring | A summary-making docstring on each view, a description on the Blueprint |
| 14 | CI integration | Did you statically generate openapi.json in CI and connect it to client type generation? |
| 15 | Schema selection | Did you choose marshmallow (smorest) or Pydantic (APIFlask) to match your assets? |
Flask is a "core-only" framework. That's exactly why what you load onto it decides production quality. Automating OpenAPI docs is an "outside-the-core" of extremely high value to load. Write one schema, and validation, shaping, and documentation all flow from one truth — that discipline makes a trustworthy API you can hand to another team.