FastOpenAPI: автодокументация OpenAPI для разных фреймворков на Python
Всем привет! Меня зовут Никита Рыженков, и я хочу поделиться опытом разработки библиотеки FastOpenAPI. Это инструмент, который предоставляет удобный стиль маршрутизации FastAPI и автоматическую документацию OpenAPI в целый ряд Python-фреймворков. Идея родилась из личной боли: в одном проекте приходилось поддерживать OpenAPI-документацию сразу в нескольких фреймворках, и я устал дублировать решения под каждый из них. Вдохновившись лаконичностью FastAPI, я решил создать унифицированный подход для Flask, AioHttp, Sanic, Falcon, Starlette, Quart, Tornado – назвал его FastOpenAPI. В этой статье расскажу, какие проблемы решает FastOpenAPI, как он устроен внутри и как им пользоваться с примерами кода под каждый поддерживаемый фреймворк.
Зачем нужен FastOpenAPI и чем он отличается от FastAPI
Проблема: Многие популярные веб-фреймворки Python (Flask, Django, AioHttp и др.) не имеют "из коробки" красивой документации API или строгой валидации запросов. Разработчикам приходится либо вручную описывать спецификации Swagger, либо использовать сторонние библиотеки/декораторы (Flask-RESTX, Spectree, drf-yasg и т.п.), что зачастую громоздко. FastAPI решает эту проблему элегантно, но связан с собственным фреймворком на базе Starlette – не всегда есть возможность или желание переписывать существующий проект на FastAPI.
Решение FastOpenAPI: Это лёгкая надстройка, которая не заменяет ваш фреймворк, а дополняет его. Вы подключаете FastOpenAPI к своему приложению (будь то Flask, Sanic, и т.д.) и описываете хендлеры API так же, как в FastAPI – через декораторы @router.get()
, @router.post()
и Pydantic-модели. На выходе получаете автогенерированный OpenAPI (Swagger) и удобную интерактивную документацию. Все входные данные автоматически валидируются, а ответы сериализуются на основе схем Pydantic.
Отличие от FastAPI: FastOpenAPI – не самостоятельный фреймворк, а библиотека, которая внедряется в существующий фреймворк. Например, FastAPI сам по себе запускает ASGI-приложение, а FastOpenAPI интегрируется с вашим Flask(__name__)
или Sanic("app")
. По сути, FastOpenAPI предоставляет единый стиль маршрутизации и генерации OpenAPI для разных фреймворков, сохраняя их подкапотную реализацию. В отличие от FastAPI, который жёстко привязан к Starlette, FastOpenAPI работает поверх нескольких фреймворков, унифицируя кодовую базу. Также, по сравнению с альтернативами вроде Flask-RESTX, Flask-Smorest или Spectree, синтаксис FastOpenAPI проще и лаконичнее, т.к. не нужно вручную описывать схему или регистрировать ресурсы, всё происходит автоматически на основе типов данных и моделей.
Кратко о возможностях:
FastAPI-стиль декораторов для маршрутов (get/post/...): можно объявлять эндпоинты так же, как в FastAPI, но на любом поддерживаемом фреймворке.
Автоматическая генерация схемы OpenAPI – JSON-файл
/openapi.json
со всеми путями, параметрами и схемами на основе ваших маршрутов.Интерактивная документация – сразу получаете Swagger UI (
/docs
) и ReDoc (/redoc
) интерфейсы, как в FastAPI, без дополнительной настройки.Валидация запросов и сериализация ответов через Pydantic v2. Никакого повторяющегося кода проверки типов – FastOpenAPI сам проверит, что к вам пришло, и превратит Pydantic-модель ответа в JSON.
Единый формат ошибок: все исключения, связанные с неверными запросами, автоматически возвращаются как JSON с описанием ошибки и корректным HTTP-статусом (400, 422, 500 и т.д.).
Поддержка нескольких фреймворков: на данный момент AioHttp, Falcon, Flask, Quart, Sanic, Starlette, Tornado. Можно использовать FastOpenAPI в любом из них одинаково, меняется лишь класс Router.
FastOpenAPI сейчас находится в стадии активной разработки (версия 0.6.0), но уже вполне работоспособен. Далее рассмотрим устройство библиотеки и примеры кода для каждого фреймворка.
Быстрый старт: как объявлять маршруты с FastOpenAPI
Главная сущность – это Router. В FastOpenAPI у каждого фреймворка есть свой класс Router, который знает, как прикрепиться к конкретному приложению. Например, для Flask – FlaskRouter
, для Sanic – SanicRouter
и т.д. Все они наследуются от базового класса BaseRouter
и разделяют общую логику.
Чтобы начать, нужно создать приложение своего фреймворка и передать его Router'у FastOpenAPI. Затем вместо привычных методов фреймворка (app.route
или app.add_route
) мы будем использовать методы Router: router.get()
, router.post()
и т.п., очень похожие на FastAPI.
Вот как выглядит минимальный пример для каждого фреймворка:
from flask import Flask
from pydantic import BaseModel
from fastopenapi.routers import FlaskRouter
app = Flask(__name__)
router = FlaskRouter(app=app)
class HelloResponse(BaseModel):
message: str
@router.get("/hello", tags=["Example"], status_code=200, response_model=HelloResponse)
def hello(name: str):
"""Приветственный endpoint на Flask"""
return HelloResponse(message=f"Привет, {name}! Это Flask!")
if __name__ == "__main__":
app.run(port=8000)
Этот код создаёт обычное Flask-приложение, но маршруты описаны через FlaskRouter
. Декоратор @router.get("/hello")
зарегистрирует GET-эндпоинт по пути /hello
. Благодаря Pydantic-модели HelloResponse
мы сразу описываем схему ответа (поле message: str
), а тип параметра name: str
в функции указывает на тип query-параметра. FastOpenAPI соберёт эти данные и включит в спецификацию OpenAPI: что у GET /hello
есть строковый query-параметр name
и ответ 200 с JSON-объектом HelloResponse
.
При запуске приложения автоматически станут доступны: Swagger UI по адресу http://127.0.0.1:8000/docs
и ReDoc по адресу http://127.0.0.1:8000/redoc
. Вы можете открыть эти страницы в браузере и увидеть сгенерированную документацию, схожую с тем, что делает FastAPI.
Разумеется, всё интерактивно и можно поклацать...
Если по каким-то причинам больше привлекает ReDoc, то он тоже прекрасно стартовал.
AIOHTTP
from aiohttp import web
from pydantic import BaseModel
from fastopenapi.routers import AioHttpRouter
app = web.Application()
router = AioHttpRouter(app=app)
class HelloResponse(BaseModel):
message: str
@router.get("/hello", tags=["Example"], status_code=200, response_model=HelloResponse)
async def hello(name: str):
"""Приветственный endpoint на AioHTTP"""
return HelloResponse(message=f"Привет, {name}! Это AioHTTP!")
if __name__ == "__main__":
web.run_app(app, host="127.0.0.1", port=8000)
Для AioHTTP код почти такой же, разница только в том, что обработчики могут быть асинхронными (async def
). Мы создаём AioHttpRouter
и регистрируем на нём GET-маршрут. После запуска web.run_app
также будут доступны Swagger UI и ReDoc. FastOpenAPI сам добавляет нужные хендлеры в приложение AioHTTP, которые отдают статику документации и JSON схемы.
Обратите внимание: синтаксис маршрута /hello
с фигурными скобками для параметров единый для FastOpenAPI. Например, если бы был путь /user/<id>
, в OpenAPI-документации он отобразится как /user/{id}
, а внутри AioHTTP он тоже правильно распознается (AioHTTP по умолчанию поддерживает {param}
в путях). В некоторых фреймворках, как увидим дальше, FastOpenAPI будет автоматически конвертировать этот синтаксис в нужный (например, Flask требует <param>
).
Sanic
from sanic import Sanic
from pydantic import BaseModel
from fastopenapi.routers import SanicRouter
app = Sanic("MySanicApp")
router = SanicRouter(app=app)
class HelloResponse(BaseModel):
message: str
@router.get("/hello", tags=["Example"], status_code=200, response_model=HelloResponse)
async def hello(name: str):
return HelloResponse(message=f"Привет, {name}! Это Sanic!")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
Пример для Sanic похож на AioHTTP (Sanic тоже async-фреймворк). Разница внутри: Sanic ожидает синтаксис маршрутов с <param>
вместо {param}
. Но об этом беспокоиться не нужно, т.к. FastOpenAPI сам конвертирует фигурные скобки в нужный формат при регистрации. Например, путь "/hello"
не содержит параметров, он останется как есть, а если бы был "/user/{id}"
, FastOpenAPI зарегистрировал бы его в Sanic как "/user/<id>".
Таким образом достигается совместимость с различными фреймворками.
Falcon
import falcon.asgi
from pydantic import BaseModel
from fastopenapi.routers import FalconRouter
app = falcon.asgi.App()
router = FalconRouter(app=app)
class HelloResponse(BaseModel):
message: str
@router.get("/hello", tags=["Example"], status_code=200, response_model=HelloResponse)
async def hello(name: str):
return HelloResponse(message=f"Привет, {name}! Это Falcon!")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
Falcon – фреймворк, в котором маршруты обычно привязаны к классам-ресурсам с методами on_get
, on_post
и т.д. FastOpenAPI абстрагирует эту особенность: вы пишете декоратор @router.get()
на обычной асинхронной функции, а внутри FalconRouter сгенерирует нужный Falcon-ресурс.
При регистрации маршрута FastOpenAPI создаёт динамический класс ресурса и добавляет ему метод on_get
, который вызывает вашу функцию hello.
Таким образом, Falcon работает с обычным ресурсом, а фактически внутри проксируется вызов нашей функции с валидацией параметров. Этот ресурс кэшируется для данного пути, и если вы добавите ещё, скажем, @router.post
для того же пути, FastOpenAPI добавит в тот же ресурс метод on_post
.
В примере выше мы запускаем Falcon-приложение через Uvicorn (ASGI). Документация Swagger/Redoc также доступна на /docs
и /redoc
– FalconRouter регистрирует три дополнительных ресурса: для выдачи /openapi.json
(схемы), HTML со Swagger UI и HTML с ReDoc.
Starlette
from starlette.applications import Starlette
from pydantic import BaseModel
from fastopenapi.routers import StarletteRouter
app = Starlette()
router = StarletteRouter(app=app)
class HelloResponse(BaseModel):
message: str
@router.get("/hello", tags=["Example"], status_code=200, response_model=HelloResponse)
async def hello(name: str):
return HelloResponse(message=f"Привет, {name}! Это Starlette!")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)
Starlette – асинхронный фреймворк (на нём же базируется FastAPI). Интеграция здесь тоже прямая: создаём Starlette-приложение и передаём в StarletteRouter
. Когда вы декорируете функцию hello
, FastOpenAPI строит объект маршрута Starlette (starlette.routing.Route
) с указанием пути, метода и ссылающийся на внутреннюю функцию-обёртку. Эта обёртка (view) делает всё то же самое: читает request.query_params
, request.body()
и вызывает router.resolve_endpoint_params
перед исполнением логики . Потом Route добавляется в app.router.routes
Starlette-приложения. В итоге Starlette обрабатывает запросы штатно, но внутри вызывает код FastOpenAPI, который проведёт валидацию и вернёт готовый JSON-ответ через starlette.responses.JSONResponse
.
Поскольку FastAPI сам по себе по сути является оболочкой над Starlette для генерации схем, интересно, что FastOpenAPI позволяет получить похожий результат без FastAPI. При этом можно тонко контролировать своё Starlette-приложение, добавлять middleware и пр., используя FastOpenAPI только для документации и валидации.
Quart
Quart – это "асинхронный Flask", и поддержка в FastOpenAPI реализована почти так же, как Flask. Используется QuartRouter
, а код функции отличается тем, что view_func
объявлен async def
и внутри него используются await
для чтения JSON из запроса и вызова обработчика. Но для пользователя всё выглядит идентично примеру с Flask, за тем исключением, что при запуске через app.run()
Quart внутри себя разворачивает event loop. Если вы знакомы с Flask, то освоить Quart с FastOpenAPI не составит труда.
Tornado
from tornado.web import Application
from pydantic import BaseModel
from fastopenapi.routers.tornado import TornadoRouter
app = Application()
router = TornadoRouter(app=app)
class HelloResponse(BaseModel):
message: str
@router.get("/hello", tags=["Example"], status_code=200, response_model=HelloResponse)
def hello(name: str):
return HelloResponse(message=f"Привет, {name}! Это Tornado!")
# Запуск Tornado сервера
if __name__ == "__main__":
import asyncio
app.listen(8000)
asyncio.run(asyncio.Event().wait())
Tornado – довольно низкоуровневый фреймворк, где маршруты представляют собой соответствие URL-шаблонов и классов-наследников RequestHandler
. В FastOpenAPI для Tornado сделана особенно интересная интеграция. Когда вы вызываете TornadoRouter(app=app)
, он не сразу регистрирует маршруты, а сохраняет их во внутренней структуре. При декорации функций FastOpenAPI строит регулярные выражения для путей (Tornado ожидает regex). Например, путь /hello
превращается в regex ^/hello$
, а путь /user/{id}
– в ^/user/(?P<id>[^/]+)$
. Эти шаблоны привязываются к специальному классу TornadoDynamicHandler
, который унаследован от tornado.web.RequestHandler
.
TornadoDynamicHandler
универсален: при запросе он смотрит на HTTP-метод и вызывает соответствующую вашу функцию-обработчик через router.resolve_endpoint_params
. В его коде метод prepare()
парсит JSON из тела запроса, а метод get()
/post()
/... просто вызывают общий метод handle_request()
. В handle_request
происходит то же знакомое: сбор параметров, валидация, вызов целевого endpoint, обработка ошибок. Затем формируется ответ через стандартные методы Tornado (self.write()
и установка status_code
и Content-Type).
Когда все маршруты объявлены, TornadoRouter регистрирует их в приложении Tornado вызовом app.add_handlers
сразу пачкой. Документация /docs
, /redoc
, /openapi.json
также регистрируется как три Tornado-хендлера с соответствующими путями.
Итого, для Tornado FastOpenAPI фактически реализует свой маршрутизатор поверх Tornado: использует регулярные выражения для URL и один handler на несколько методов. Это сделано, чтобы сохранить семантику FastAPI (несколько методов на один путь) – TornadoRouter накапливает все методы в _endpointmap
и при добавлении нового метода к уже существующему пути просто обновляет карту, без регистрации нового маршрута.
Под капотом FastOpenAPI: как это работает?
Мы увидели, что синтаксис использования для разных фреймворков очень похож, и это заслуга общего базового класса. Давайте заглянем внутрь некоторых ключевых частей FastOpenAPI. Основные компоненты: BaseRouter, система генерации OpenAPI-схем, валидация/сериализация данных, обработчик ошибок и эндпоинты документации Swagger/Redoc.
BaseRouter и регистрация маршрутов
Все Router-классы наследуют BaseRouter
. BaseRouter хранит в себе список всех зарегистрированных маршрутов _routes
и отвечает за генерацию итоговой схемы OpenAPI. При инициализации BaseRouter можно передать параметр app
– экземпляр приложения фреймворка. Если app
передан, BaseRouter сразу добавит к нему маршруты документации (о них ниже).
Когда мы декорируем функцию как @router.get("/path", ...)
, происходит следующее: метод BaseRouter.get()
возвращает декоратор, который помечает функцию атрибутом __route_meta__ = meta
(meta содержит наши аргументы: tags, response_model, status_code и др.) и затем вызывает self.add_route(path, "GET", func)
. В BaseRouter add_route
просто кладёт кортеж (path, method, endpoint)
в список _routes
. Непосредственно привязкой к фреймворку BaseRouter не занимается – этим ведает конкретный Router.
Например, FlaskRouter.add_route
вызывается вместо базового (он его перекрывает) и делает два шага: сперва вызывает super().add_route()
(запись в список, для генерации схемы), а потом регистрирует route в самом Flask-приложении через app.add_url_rule
. В большинстве Router-ов шаблоны путей преобразуются: Flask/Quart/Sanic ожидают <param>
, Tornado – (?P<param>regex)
, Falcon и AioHTTP позволяют {param}
. В коде FlaskRouter, например, видно замену фигурных скобок на формат Flask: flask_path = re.sub(r"{(\w+)}", r"<\1>", path)
. Затем формируется функция-обёртка view_func
, которая принимает все параметры запроса (query, path, body) и вызывает внутренние методы FastOpenAPI для валидации и исполнения логики. Наконец, эта функция регистрируется во Flask через app.add_url_rule(flask_path, endpoint_name, view_func, methods=[...])
.
Для async-фреймворков (AioHTTP, Starlette, Sanic) схожая схема: создаётся view = functools.partial(... view, router=self, endpoint=...)
, т.е. ссылка на универсальную функцию обработки, привязанную к конкретному маршруту. Дальше вызывается нативный метод фреймворка: например, AioHTTP app.router.add_route(method, path, view)
, Sanic app.add_route(view, path, methods=[...])
, Starlette – добавляется объект Route
в список роутов приложения. Таким образом, FastOpenAPI не меняет основной цикл обработки запросов: он встраивается в него через callback’и.
Отдельно стоит отметить метод include_router()
– он позволяет вложить один Router в другой, аналогично FastAPI.include_router
. Это удобно для структурирования больших API (можно сделать роутеры по подсистемам, а потом собрать под общим префиксом). Реализация простая: include_router
берёт все маршруты из другого роутера other.get_routes()
и добавляет их к текущему с указанным префиксом. Метаданные (tags, модели) при этом сохраняются, просто пути получат префикс.
Генерация OpenAPI-схемы и документации
Ключевой момент – как из этих зарегистрированных маршрутов получается схема OpenAPI. FastOpenAPI делает это в момент первого запроса к /openapi.json
или при первом обращении к свойству router.openapi
. В BaseRouter определён метод generate_openapi()
, который пробегается по всем _routes
и формирует словарь согласно спецификации OpenAPI.
Основные шаги в generate_openapi
:
Шапка документа: Заполняются поля
openapi
(версия спецификации, по умолчанию "3.0.0"),info
(сюда берутся заголовок, описание и версия API, которые можно передать при инициализации BaseRouter). Если вы ничего не передали, будут дефолтные: title = "My App", version = "0.1.0", description = "API documentation".Определения компонентов: Создаётся пустой раздел
"components": {"schemas": {}}
. Туда будут собираться схемы Pydantic-моделей и ошибок. FastOpenAPI сразу добавляет одну стандартную схему ошибки –ErrorSchema
. Это общая структура JSON-ошибки: полеerror
с вложенными полямиtype
,message
,status
,details
. Все встроенные исключения FastOpenAPI возвращают ошибки именно в таком формате.Paths и Operations: Для каждого маршрута
(path, method, endpoint)
выполняется:Преобразование пути в формат OpenAPI: заменяются фреймворк-специфичные шаблоны на стандартные
{param}
. Например, stored path может быть/users/<id>
(если вы добавили через FlaskRouter), его сконвертируют обратно в/users/{id}
для документации.Вызов
_build_operation(endpoint, definitions, openapi_path, method)
- этот метод собирает описание функции (ендпоинта) в формате OpenAPI: параметры, тело запроса, ответы, тег и пр.
Разберём, как строится описания параметров и тела запроса. В _build_operation
он делегирует в _build_parameters_and_body
. Внутри _build_parameters_and_body
происходит анализ сигнатуры функции endpoint
с помощью inspect.signature
. Для каждого параметра функции:
Если тип аннотации – подкласс
BaseModel
(Pydantic-модель), то различаются случаи:Маршрут GET: Pydantic-модель трактуется как контейнер query-параметров. Код пробегается по всем полям модели и добавляет каждое как отдельный параметр
"in": "query"
. Обязательные/необязательные берутся из.required
модели. Например, если у васdef endpoint(filter: FilterModel)
, где FilterModel имеет поля page: int = 1, size: int = 10, то в OpenAPI эти два поля появятся как ?page=integer, ?size=integer, оба не обязательны (есть дефолтные).Маршрут не GET: Pydantic-модель считается телом запроса (JSON). Тогда в OpenAPI она описывается единым объектом в
requestBody
. FastOpenAPI берет JSON-схему модели (метод Pydanticmodel_json_schema
) и вставляет её как схему дляapplication/json
контента. Поле"required"
для requestBody ставится на true, если параметр функции обязателен (нет default=None).
Если тип параметра не Pydantic-модель:
Определяется его расположение: если имя параметра присутствует в шаблоне пути (например,
id
в/user/{id}
), тоin: path
, иначеin: query
.Определяется тип схемы: для базовых типов Python использует маппинг
int -> integer
,bool -> boolean
и т.д. (PYTHON_TYPE_MAPPING
). Если тип не распознан, по умолчанию ставится"string"
.Формируется запись параметра: имя, расположение, обязательность (все path-параметры обязательны по OpenAPI стандарту; query – обязательны если нет default).
Возвращается список параметров и (опционально) описание requestBody. Если ни один параметр не был Pydantic-моделью, requestBody будет None.
Далее _build_operation
собирает блок responses – возможные ответы эндпоинта. Код _build_responses
обрабатывает параметр декоратора response_model
и формирует схему ответа 200 (или указанного status_code).
Логика такая:
Если
response_model
– этоList[Model]
(список Pydantic-моделей), то в схему ответа ставитсяtype: array
сitems
как схема элемента (сама модель).Если
response_model
– класс Pydantic BaseModel, то просто берётся его JSON-схема (черезgetmodel_schema
).Если указали базовый тип (например,
response_model=int
), то подставится соответствующий примитивный тип ("type": "integer"
).Иначе, если передали что-то неожиданное, выбрасывается Exception "Incorrect response_model", то есть FastOpenAPI ожидает либо Pydantic-модель, либо тип, либо список этих вариантов.
Кроме успешного ответа, FastOpenAPI может документировать стандартные ошибки. Декоратор позволяет указать response_errors=[400, 404, ...]
для перечисления возможных ошибок. Метод _build_error_responses
возвращает словарь с описаниями ошибок 400,401,403,404,422,500, каждая из которых содержит ссылку на общую схему ErrorSchema
. По умолчанию (если response_errors
не задан) метод вернёт пустой словарь, чтобы не загромождать спецификацию. Но даже без явного указания, схема ошибок будет доступна в секции компонентов (на случай если клиентский код захочет знать формат ошибки).
После сборки responses (успешного и ошибок) формируется окончательный объект operation: прописывается "responses": { ... }
, "parameters": [ ... ]
(если они есть), "requestBody"
(если есть). Также добавляются "tags"
из _meta_
и описание (из docstring функции, если написать строку в тройных кавычках в обработчике, FastOpenAPI её извлечёт и поместит как описание метода в OpenAPI) – это происходит в b_и-_build_operation
, мы видим что берётся endpoint.__doc__
при наличии (в нашем коде docstring "Приветственный endpoint..." попадёт в описание функции).
Когда функция готова, generate_openapi
вставляет её в структуру под соответствующий путь и метод. После обработки всех маршрутов, все собранные определения моделей складываются в components.schemas
. Тут важно отметить, что Pydantic v2 возвращает JSON-схему, которая может содержать вложенный блок $defs
для под-моделей. FastOpenAPI объединяет эти определения в общий словарь definitions
при помощи _get_model_schema
и кеширует, чтобы не генерировать повторно одинаковые схемы.
Кеш (_model_schema_cache) хранится на уровне класса BaseRouter, чтобы избежать генерирования одних и тех же схем. Когда проискходит первый вызов документации, то каждая схема генерируется один раз и попадает в кеш. Если какие-то методы используют одни и те же схемы, то генерация будет один раз. После полной генерации всей openapi-схемы кеш очищается, а сгенерированная схема кешируется уже в _openapi_schema
.Таким образом, при каждом новом запросе к OpenAPI-схема повторные тяжелые операции генерации Pydantic-схем происходить не будут.
Отдельно стоит упомянуть внедрение Swagger UI и ReDoc. BaseRouter содержит константы с CDN-ссылками на нужные скрипты UI: SWAGGER_URL
и REDOC_URL
. В BaseRouter реализованы методы render_swagger_ui(openapi_url)
и render_redoc_ui(openapi_url)
, которые возвращают простой HTML с подключением этих CDN и указанием, откуда брать JSON-схему. Например, Swagger UI – это html-страничка с <div id="swagger-ui"></div>
и подключением swagger-ui-dist.js, которая сама подтянет нужный OpenAPI JSON. При инициализации Router, если docs_url
/redoc_url
не отключены, вызывается _register_docs_endpoints()
. Этот метод в каждом Router-классе свой, но суть одна: зарегистрировать три маршрута:
openapi_url
(по умолчанию/openapi.json
) – возвращает JSON-схему (контент application/json).docs_url
(по умолчанию/docs
) – возвращает HTML со Swagger UI.redoc_url
(по умолчанию/redoc
) – возвращает HTML с ReDoc.
Валидация запросов и формирование ответов
Одно из самых больших преимуществ FastOpenAPI – автоматическая обработка входных данных. В каждом Router, внутри view-функции, после извлечения параметров запроса вызывается router.resolve_endpoint_params(endpoint, all_params, body)
. Этот метод реализован в BaseRouter и является сердцем валидации. Он смотрит сигнатуру endpoint
и пытается собрать для неё kwargs:
Если параметр функции ожидает Pydantic-модель, происходит попытка создать эту модель из данных. Код делает
model_instance = Model(**params)
– гдеparams
берутся либо из тела JSON, либо (если тело пустое) из всех параметров сразу. Если модель не валидируется (Pydantic выбросит ошибку), то перехватывается Exception и выбрасывается специальное API-исключениеValidationError
с сообщением ошибки Pydantic. Это приводит к тому, что пользователь получит 422 Unprocessable Entity с деталями, что именно не прошло валидацию.Если параметр примитивного типа (int, float, bool и пр.), берётся значение из
all_params
(словарь объединивший query и path параметры). Далее пытается привести к нужному типу, просто вызвав конструктор типа: например,int("abc")
бросит ValueError. Такой промах ловится и генерируетсяBadRequestError
– 400 с сообщением о неверном формате параметра. Таким образом, если клиент вместо числа передал строку где ожидалось число, ответ будет стандартный 400 Bad Request.Если параметр необязательный и не был передан, то подставляется значение по умолчанию, а если обязательный и отсутствует, то тоже выбрасывается BadRequestError "Missing required parameter ...".
В итоге resolve_endpoint_params
возвращает словарь kwargs
, готовый для передачи функции-обработчику. Если на этом этапе было исключение, оно транслируется как API-ошибка. Эти ошибки не сырые Exceptions, а экземпляры классов, определённых в fastopenapi.error_handler
. Например, BadRequestError
и ValidationError
наследуют от общего APIError
и несут в себе поле status_code
(400 и 422 соответственно).
После успешной валидации и получения kwargs
Router вызывает сам endpoint: result = endpoint(**kwargs)
. Если это coroutine (async), то выполняется с await
(в sync-фреймворках просто вызов). Тут тоже может быть выброшено исключение, например, сам код обработчика может решить выбросить ResourceNotFoundError
или обычный ValueError. Любое исключение на этом этапе ловится и обрабатывается через router.handle_exception(e)
. handle_exception
– обёртка над функцией format_exception_response
из error_handler.py
. Она пытается привести Exception к стандартному виду:
Если это уже
APIError
(одно из наших), просто вызвать.to_response()
– метод, возвращающий словарь с полями error/type/message/status.Если это исключение фреймворка (например, Flask может бросать
werkzeug.NotFound
с кодом, Starlette —HTTPException
), функция вытащит из него атрибутыstatus_code
илиcode
, а такжеdescription
илиdetail
для сообщения. Затем она создастgeneric_error = APIError(message=..., details=...)
и установит у него статус и тип (тип определяется по маппингу: например, 404 -> ErrorType.RESOURCE_NOT_FOUND). После чего вернётgeneric_error.to_response()
. То есть даже неизвестные исключения превратятся в структурированный ответ.Таким образом на выходе
handle_exception
всегда даёт JSON вида:{ "error": { "type": "VALIDATION_ERROR", "message": "Validation error for parameter 'x'", "status": 422, "details": "Value is not a valid integer" } }
Примерно такое получит клиент, если отправит неправильный запрос (например, текст вместо числа). Это гораздо лучше беспорядочных стектрейсов, поскольку API всегда ответит понятным клиенту сообщением об ошибке. Формат унифицирован для всех фреймворков.
После успешного выполнения функции-обработчика (если исключений нет), получаем result
. Он может быть Pydantic-моделью, dataclass'ом, обычным dict/list/primitive. FastOpenAPI приводит его к сериализуемому виду через _serialize_response(result)
. Этот метод рекурсивно обходит объект:
Если это Pydantic
BaseModel
, вызывает.model_dump()
и выдаёт словарь.Если список – сериализует каждый элемент.
Если словарь – сериализует каждое значение.
Примитивы и прочие типы оставляет как есть.
Наконец, view_func
возвращает готовый ответ через механизм фреймворка. Например, во Flask это jsonify(result), status_code
, в Sanic – response.json(result, status=...)
, в Starlette – JSONResponse(result, status_code=...)
, Tornado – пишет в self.write
и ставит статус. Код берёт status_code
из метаданных маршрута (что мы указали в декораторе, например 201) либо по умолчанию 200. Если указан status_code=204
(No Content), FastOpenAPI особым образом обрабатывает: не пытается сериализовать результат и возвращает пустой ответ.
Подводя итог: FastOpenAPI берет на себя всю черновую работу проверки типов, приведения входных данных, а также приведение выходных данных к JSON. Вам, как разработчику, можно писать обработчик так, будто он всегда получает правильные Python-объекты нужных типов – все проверки уже сделаны.
Обработка ошибок и единый формат ответов
Мы уже коснулись error_handler. Расскажу чуть подробнее о системе ошибок. В fastopenapi.error_handler
определены классы исключений:
BadRequestError (400)
AuthenticationError (401)
AuthorizationError (403)
ResourceNotFoundError (404)
ResourceConflictError (409)
ValidationError (422)
InternalServerError (500)
ServiceUnavailableError (503)
Все они наследуют APIError
, у которого заданы базовые поля:
status_code
(по умолчанию 500, но в наследниках переопределён на нужный).default_message
(человекочитаемое сообщение по умолчанию).error_type
– элемент перечисленияErrorType
(строки вроде "BAD_REQUEST", "VALIDATION_ERROR" и т.д.).
Когда вы вызываете, например, raise ResourceNotFoundError("User not found")
внутри своего endpoint, FastOpenAPI перехватит это и поймёт, что это APIError. Метод to_response
у APIError сформирует словарь:
{
"error": {
"type": "RESOURCE_NOT_FOUND",
"message": "User not found",
"status": 404
}
}
Этот словарь и уйдёт клиенту с HTTP статусом 404. Важно: такие ошибки можно выбрасывать где угодно в вашем коде, и не надо оборачивать их в Response – FastOpenAPI сам превратит их в корректный HTTP-ответ.
Если выбрасывается неожиданное исключение (не потомок APIError), format_exception_response
постарается отнести его к одной из категорий. Например, если внутри Flask выкинули abort(401)
, то прилетит исключение с полем .code = 401
и .name = "Unauthorized"
. FastOpenAPI это распознает и сконструирует APIError
с типом AUTHENTICATION_ERROR и сообщением "Unauthorized". Таким образом, даже специфичные для фреймворка штуки (как werkzeug.exceptions
) конвертируются. Если не удалось вообще ничего выяснить, исключение попадёт в категорию 500 INTERNAL_SERVER_ERROR.
Единый формат ошибок облегчает жизнь потребителям API: фронтенд-разработчики или внешние пользователи вашего API всегда могут ожидать поле "error"
в ответе и не гадать, как именно ошибка описана.
Swagger UI и ReDoc
В конце хотелось бы подчеркнуть, насколько легко FastOpenAPI подключает интерфейсы документации. Вам не нужно самим устанавливать swagger-ui
или писать HTML – при инициализации Router достаточно оставить параметры по умолчанию (docs_url="/docs", redoc_url="/redoc", openapi_url="/openapi.json"
). Библиотека добавит три маршрута:
GET
/openapi.json
– выдаёт сгенерированную JSON-схему. Реализовано тривиально, например в AioHttpRouter:return web.json_response(self.openapi)
.GET
/docs
– возвращает готовую HTML-страницу с подключением Swagger UI. В коде подставляется нужный URL JSON-схемы (по умолчанию/openapi.json
) в шаблон HTML. Swagger UI при открытии сразу подтянет схему и отрисует её.GET
/redoc
– аналогично, страница с подключением ReDoc JS, который отрисует красивую документацию.
Реакция сообщества и применение на практике
Проект FastOpenAPI появился совсем недавно (в марте 2025 года), но уже привлёк внимание разработчиков. Пост «Show HN: FastOpenAPI – automated docs for many Python frameworks» набрал заметное число голосов на Hacker News. Многим оказалась близка идея “FastAPI-подобная штука для других фреймворков”. В комментариях отмечали, что это полезно для поддержки легаси-приложений, подчёркивали его простоту интеграции и то, что он построен на современном Pydantic v2. Кому-то идея помогла взглянуть на знакомые фреймворки под другим углом.
GitHub репозиторий уже набрал порядка 400 звезд и несколько форков всего за первый месяц. В открытом доступе пока не так много публичных проектов, использующих FastOpenAPI (всё-таки проект моложе нескольких месяцев), но растущее число звёзд и установки с PyPI говорят о том, что разработчики много экспериментируют с библиотекой.
Стоит отметить, что проект активно поддерживается. Есть документация на английском и русском языках на сайте fastopenapi.fatalyst.dev, где можно найти дополнительные примеры и ответы на часто задаваемые вопросы.
Обратная связь приветствуется – можно создавать issues на GitHub, предлагать поддержку новых фреймворков или сообщать об ошибках.
Выводы: когда применять FastOpenAPI
FastOpenAPI уже сейчас может быть полезен, если у вас:
Существующее приложение, которое вы не хотите или не можете переписать на FastAPI, но хотели бы иметь подобную документацию и валидацию. С FastOpenAPI вы постепенно оборачиваете существующие обработчики декораторами Router'а – и получаете удобство типизированного API без большой переделки.
Несколько фреймворков в работе. Бывают ситуации, когда разные части системы написаны на разных фреймворках (Flask и aiohttp, например). FastOpenAPI поможет привести их к единообразию в части описания API. Это уменьшает когнитивную нагрузку: разработчики переключаются между проектами, а шаблон работы одинаковый.
Библиотека или плагин, поддерживающий разные фреймворки. Если вы разрабатываете что-то, что должно встраиваться в чужие приложения (скажем, готовый модуль с REST API для какого-то сервиса), с FastOpenAPI можно сразу поддержать несколько хостов. Вместо того, чтобы писать отдельные классы для Flask, Sanic и т.д., можно использовать общий код и просто инициализировать соответствующий Router в зависимости от окружения.
Плюсы библиотеки:
Единый и знакомый интерфейс маршрутизации (для тех, кто знает FastAPI).
Снижение количества шаблонного кода: Pydantic модели определяют и валидацию, и документацию сразу.
Упорядоченная, консистентная документация API, которую легко предоставить внутренним или внешним пользователям.
Модульность: FastOpenAPI не заставляет вас менять стиль написания всей логики, вы добавляете его так же, как, например, подключили бы Flask-RESTX, но получаете более современный подход.
Поддержка асинхронности там, где это нужно (AioHTTP, Sanic, Starlette, Quart, Tornado) и совместимость с синхронными фреймворками (Flask, Falcon).
Минусы и ограничения:
Проект ещё молод и находится в версии 0.6.0. Возможны изменения API библиотеки, некоторые функции могут быть не до конца отлажены.
Пока не поддерживаются некоторые фреймворки из коробки. Однако архитектура позволяет относительно легко добавить новый Router – сообщество может присоединиться и расширить список (например, возможно появится поддержка Django).
Добавление прослойки может незначительно влиять на производительность. Если ваше приложение критично к максимально быстрому прохождению запросов, учитывайте, что каждый запрос проходит через Pydantic валидацию. Pydantic v2 достаточно быстрый, и для большинства случаев оверхед оправдан удобством, но в теории прямой код фреймворка без доп. абстракций будет чуть быстрее. Тем не менее, разница обычно некритична, и вы всегда можете профилировать конкретно ваш сценарий.
Интеграция с некоторыми специфическими особенностями фреймворков может требовать дополнительных усилий. Например, специфические middleware, сложные паттерны роутинга или нестандартные типы запросов (WebSocket, Server-Sent Events) FastOpenAPI не покрывает – он ориентирован на REST HTTP JSON APIs.
Для некоторых фреймворков интеграция в существующий проект будет крайне трудозатратна (Например, в Falcon используются классы, а тут всё на основе функций).
В заключение, FastOpenAPI – отличный компромисс, на мой взгляд, для тех, кто хочет удобства FastAPI, но не хочет привязываться к FastAPI полностью. Вы можете постепенно внедрять его в существующий проект: начать с документации для нескольких хендлеров, посмотреть, как это выглядит, написать автотесты на валидные/невалидные запросы. Возможно, вам понравится полученный опыт, и вы распространите подход на весь сервис.