Fast API
FastAPI⚑
Переваги та недоліки FastAPI⚑
Переваги FastAPI
- Швидкість і ефективність: FastAPI заснований на асинхронному програмуванні, що дозволяє обробляти велику кількість запитів на секунду.
- Простота і зручність використання: FastAPI має простий та інтуїтивно зрозумілий інтерфейс, який дозволяє швидко і легко створювати веб-додатки.
- Автоматична документація: FastAPI автоматично генерує документацію для API на основі анотацій Python, що спрощує роботу з API.
- Підтримка OpenAPI і JSON Schema: FastAPI підтримує стандарти OpenAPI і JSON Schema, що дозволяє використовувати різні інструменти для роботи з API.
- FastAPI використовує особливості мови Python, такі як анотації типів, для підвищення продуктивності та зручності розробки.
Недоліки FastAPI
- Необхідність вивчення асинхронного програмування: FastAPI використовує асинхронне програмування, що може бути складним для новачків у Python.
- Відсутність підтримки деяких функцій: FastAPI ще не підтримує деякі функції, які доступні в інших веб-фреймворках. Кількість бібліотек менша ніж в екосистемі Django.
Що приймає Depends() у FastAPI? Якого типу об'єкт це з погляду Python?⚑
Summary
Depends()приймає будь-якийcallable(функцію, корутину, клас, об'єкт з__call__) і повертає примірник внутрішнього класуfastapi.params.Depends. Це маркер, який FastAPI шукає у сигнатурах ендпоінтів і за яким будує граф залежностей.
Depends() сам по собі не викликає передану функцію - він лише обгортає її в маркерний об'єкт. Виклик відбувається пізніше, коли FastAPI розв'язує залежності для кожного запиту.
from fastapi import FastAPI, Depends
app = FastAPI()
def get_db():
db = Session()
try:
yield db
finally:
db.close()
@app.get("/items")
def list_items(db = Depends(get_db)):
return db.query(Item).all()
Що можна передати у Depends():
- Звичайну функцію або корутину (
async def). - Клас - FastAPI створить екземпляр на кожен запит.
- Об'єкт із реалізованим
__call__. - Інший об'єкт
Depends(вкладені залежності). Depends(None)- placeholder, корисний у тестах, коли залежність підставляється черезapp.dependency_overrides.
Що робить Depends під капотом:
- Реєструє залежність у графі - FastAPI обходить його рекурсивно.
- Підтримує
async/sync, генератори (для setup/teardown черезyield),BackgroundTasks,Request,Response. - Кешує результат у межах одного запиту (
use_cache=Trueза замовчуванням) - корисно, якщо одна залежність підставляється у кілька інших.
Різниця між Depends() та параметром dependencies=[...]⚑
Summary
Depends()як аргумент функції повертає результат і дає до нього доступ у тілі handler'а.dependencies=[Depends(...)]на рівні path-операції чиAPIRouterзапускає залежність як guard - результат не повертається, але виняток у залежності завершує запит.
Призначення кожного варіанту
Depends() як параметр функції - класичний DI: результат потрібен у тілі (наприклад, екземпляр Session, об'єкт користувача, налаштування).
dependencies=[Depends(check_admin), Depends(rate_limit)] - guard: залежність виконується перед handler'ом, її результат відкидається. Підходить для перевірок, де важливий side effect (валідація токена, перевірка дозволу, вичерпання rate-limit квоти), а не повернене значення.
Реалізація
from fastapi import Depends, FastAPI, HTTPException, Header, APIRouter
app = FastAPI()
def get_current_user(token: str = Header(...)) -> User:
user = decode_token(token)
if user is None:
raise HTTPException(401, "Invalid token")
return user
def require_admin(user: User = Depends(get_current_user)) -> None:
if not user.is_admin:
raise HTTPException(403, "Admin only")
# Result-providing dependency: user is available in the body.
@app.get("/me")
def me(user: User = Depends(get_current_user)) -> UserSchema:
return UserSchema.model_validate(user)
# Guard dependency: require_admin runs before the handler; its return value
# is discarded but an exception inside it aborts the request.
admin_router = APIRouter(
prefix="/admin", dependencies=[Depends(require_admin)]
)
@admin_router.get("/stats")
def admin_stats() -> StatsSchema:
return collect_stats()
Спільна поведінка
- Кешування (
use_cache=Trueза замовчуванням) діє в обох випадках - якщо одна й та сама залежність використовується і як аргумент, і як guard у тому самому запиті, вона виконається один раз. - Граф залежностей будується незалежно від того, як
Dependsоголошено. ВкладеніDependsпрацюють однаково.
Антипатерн
Передавати у dependencies=[Depends(...)] залежність, чий результат потім потрібен у тілі - це повторне виконання (FastAPI не може автоматично перенести результат з guard-списку в аргументи). Якщо потрібен результат - оголошувати як аргумент handler'а.
Links
- FastAPI docs: Global Dependencies -
dependencies=на рівніFastAPI()іAPIRouter - FastAPI docs: Dependencies in path operation decorators
BackgroundTasks у FastAPI⚑
Summary
BackgroundTasks- вбудований механізм запуску задач після надсилання response клієнту. Задачі виконуються в тому самому процесі:async defзадача йде в event loop, синхроннаdef- у threadpool. Це не фоновий worker і не окремий потік для async-задач.
Принцип роботи
Імпортується клас BackgroundTasks з fastapi. Інстанс отримується як параметр handler'а - FastAPI створює його на кожен запит. Задачі додаються через tasks.add_task(callable, *args, **kwargs). Після завершення тіла handler'а і надсилання response Starlette проходить чергу і виконує задачі послідовно.
Виконання залежить від того, чи задача оголошена як async def чи звичайна def:
async def- запускається у тому самому event loop, що й handler. Якщо задача робить блокуючий syscall - блокує event loop.def(синхронна) - запускається у threadpool черезstarlette.concurrency.run_in_threadpool, який всередині викликаєanyio.to_thread.run_sync.
Окремий потік задіюється тільки для синхронних def-задач; async def задача виконується у тому самому event loop, що й основний handler.
Реалізація
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
async def send_email(to: str, body: str) -> None:
async with httpx.AsyncClient() as client:
await client.post("https://api.mailer/send", json={"to": to, "body": body})
@app.post("/orders")
async def create_order(
order: OrderIn,
tasks: BackgroundTasks,
repo: OrderRepo = Depends(get_repo),
) -> OrderOut:
saved = await repo.insert(order)
tasks.add_task(send_email, order.customer_email, f"Order {saved.id} created")
return OrderOut.model_validate(saved)
Response повертається клієнту одразу після return; send_email стартує вже після цього і не впливає на латентність endpoint'а.
Обмеження
- Процес-локальні. Задача живе в межах процесу, який обробив запит. Якщо процес перезавантажується (deploy, OOM-kill) - задача втрачається.
- Без retry, без persistence. Падіння задачі не призводить до повторного виконання. Logging винятків лежить на самій задачі або на global exception handler.
- Без backpressure. Велика кількість задач у пам'яті процесу не обмежена; при сплеску може вичерпати RAM або thread pool.
- Послідовне виконання у межах одного запиту. Задачі з одного
BackgroundTasksйдуть одна за одною, не паралельно.
Для задач, які мають переживати рестарт процесу, мати retry/idempotency, розподіляти навантаження і збирати метрики - канонічний шлях: винести в окремий worker (Celery, RQ, ARQ, Dramatiq) і публікувати завдання у MQ. BackgroundTasks підходить для дешевих fire-and-forget операцій (логування, надсилання телеметрії, інвалідація кешу).
Links
- FastAPI docs: Background Tasks
- Starlette source: BackgroundTasks -
await run_in_threadpool(self.func, *self.args, **self.kwargs)для sync-функцій - Starlette source: run_in_threadpool - тонкий wrapper над
anyio.to_thread.run_sync
FastAPI під капотом⚑
Summary
FastAPI - тонкий шар поверх двох незалежних бібліотек: Starlette (ASGI toolkit з routing, middleware, websockets) і Pydantic (валідація та серіалізація). Сам FastAPI не має ні HTTP-сервера, ні event loop'а - запуск виконує зовнішній ASGI-сервер (Uvicorn / Hypercorn / Granian).
Внесок Starlette
- Routing (
@app.get,@app.post, ...) - FastAPI обгортає decorators Starlette з додаванням dependency-graph і OpenAPI-метаданих. - Middleware-stack,
Request/Responseоб'єкти. - WebSocket support, Server-Sent Events.
- Сесії, GZip, CORS, TrustedHost - як стандартні middleware.
BackgroundTasks,StreamingResponse,FileResponse.TestClient(синхронний адаптер навколоhttpx).
Внесок Pydantic
BaseModelдля опису request/response схем.- Автоматична валідація вхідних даних: FastAPI парсить тіло запиту відповідно до сигнатури handler'а і повертає
422 Unprocessable Entityіз детальним списком помилок при невалідних даних. - Серіалізація вихідних даних через
model_dump(). - Генерація OpenAPI-схеми з типів - саме звідси автодокументація
/docsі/redoc.
Внесок самого FastAPI
- Граф залежностей (
Depends,dependencies=), див. розділDepends(). - Сполучення Starlette-routing'у з Pydantic-валідацією і автогенерацією OpenAPI.
APIRouterдля модуляризації.- Конвенції для security-схем (
OAuth2PasswordBearer,APIKeyHeader).
Поза межами FastAPI
- HTTP-сервер. Запускати треба ASGI-сервером:
uvicorn main:app --workers 4або через gunicorn зUvicornWorker. Деталі - у розділі розгортання. - Event loop. FastAPI делегує управління event loop'ом ASGI-серверу і стандартному
asyncio(абоuvloop, якщо встановлений). - ORM. SQLAlchemy / Tortoise / SQLModel - окремі бібліотеки; FastAPI не нав'язує жодну.
Практичні наслідки шарування
Розуміння шарів пояснює перформанс-характеристики і обмеження:
- Швидкість FastAPI на синтетичних тестах - це переважно швидкість Starlette і Uvicorn (C-розширення
httptools,uvloop); FastAPI додає невелику накладну витрату на dependency-resolution і Pydantic-валідацію. - Багато можливостей FastAPI - це Starlette-фічі (BackgroundTasks, WebSocket, middleware). Документація Starlette часто детальніша за документацію FastAPI для цих API.
Links
Middleware у FastAPI⚑
Summary
FastAPI підтримує два рівні middleware: pure ASGI middleware (рекомендовано для високого навантаження) і
BaseHTTPMiddlewareзі Starlette (зручніший API, але має overhead через обгортання у фонову task). Реєструються черезapp.add_middleware(MiddlewareClass, **opts).
Стандартні middleware
Усе зі Starlette, готове до використання:
CORSMiddleware- обробка cross-origin запитів, див. CORS.GZipMiddleware- стиснення відповідей зContent-Lengthпонад мінімум.TrustedHostMiddleware- захист від host-header injection.SessionMiddleware- cookie-based session storage.HTTPSRedirectMiddleware- примусовий редирект з HTTP на HTTPS.
Реалізація кастомного middleware
Канонічний шлях для бізнес-логіки - BaseHTTPMiddleware. Перевизначається метод dispatch(request, call_next):
import time
from collections.abc import Awaitable, Callable
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
class RequestTimingMiddleware(BaseHTTPMiddleware):
async def dispatch(
self,
request: Request,
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Response-Time-Ms"] = f"{duration * 1000:.1f}"
return response
app.add_middleware(RequestTimingMiddleware)
call_next(request) запускає решту middleware-stack'у і handler; повертає Response. Все, що до await call_next - "перед запитом", після - "після відповіді". Виняток у call_next піде через except.
ASGI middleware (для перформансу)
BaseHTTPMiddleware обгортає кожен запит у проміжну anyio task для підтримки streaming body. На високих RPS цей overhead помітний (десятки мікросекунд + додаткова алокація). Pure ASGI middleware пишеться як ASGI application (приймає scope, receive, send):
class RequestIdMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
request_id = generate_request_id()
async def send_wrapper(message):
if message["type"] == "http.response.start":
message["headers"].append(
(b"x-request-id", request_id.encode())
)
await send(message)
await self.app(scope, receive, send_wrapper)
app.add_middleware(RequestIdMiddleware)
Pure ASGI потребує знання ASGI-протоколу і не має зручних Request/Response обгорток, але уникає накладних витрат BaseHTTPMiddleware.
Порядок виконання
Middleware виконуються в зворотному порядку реєстрації для request і у прямому для response - "матрьошка" обгорток. Останній зареєстрований add_middleware - найзовнішній (виконується першим на вході, останнім на виході).
Links
- Starlette docs: Middleware - повний перелік стандартних і API
BaseHTTPMiddleware - Starlette source: BaseHTTPMiddleware - реалізація через
anyiotask
CORS у FastAPI⚑
Summary
CORS (Cross-Origin Resource Sharing) - механізм браузера для контролю запитів між різними origin'ами.
CORSMiddlewareзі Starlette додає відповідні response-заголовки і обробляє preflight (OPTIONS) запити; сам CORS - це браузерна політика, не серверний firewall.
Реалізація
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com", "https://admin.example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
max_age=600,
)
allow_origins - точний перелік дозволених origin'ів (схема + host + port). max_age - як довго браузер кешує preflight-результат (зменшує OPTIONS трафік).
Обмеження wildcard з credentials
Специфікація CORS забороняє комбінацію Access-Control-Allow-Origin: * з Access-Control-Allow-Credentials: true. Якщо потрібні credentials (cookies, Authorization-header) - allow_origins має бути явним списком, не ["*"].
CORSMiddleware валідує це у момент відповіді: при allow_origins=["*"] і allow_credentials=True хедер Access-Control-Allow-Credentials не надсилається, і браузер відкине куки.
Preflight (OPTIONS)
Для non-simple запитів (методи поза GET/HEAD/POST, custom-хедери, JSON-Content-Type) браузер спершу надсилає OPTIONS запит з заголовками Access-Control-Request-Method/-Headers. CORSMiddleware відповідає на нього сам - handler не запускається.
Межі CORS
CORS не захищає сервер від несанкціонованого доступу. Запит, який не пройшов CORS-перевірку, доходить до сервера: браузер блокує доступ до response на стороні клієнта, але сервер вже виконав handler і змінив стан. Безпека покладається на автентифікацію (токени, сесії) і авторизацію - CORS лише контролює, який JavaScript може читати відповідь.
Links
- MDN: CORS - повна специфікація політики
- Starlette docs: CORSMiddleware
Pydantic-схеми у FastAPI⚑
Summary
Pydantic-схема (підклас
BaseModel) описує форму даних на вході або виході handler'а. FastAPI використовує її для двох речей: валідації request body (вхід) іresponse_modelдля серіалізації результату (вихід).
Серіалізація і десеріалізація
- Серіалізація - перетворення Python-об'єкта у транспортний формат (JSON, bytes). У FastAPI -
model.model_dump_json()/jsonable_encoder(). - Десеріалізація - зворотне: з JSON у Python-об'єкт. Це робить
model_validate_json(raw_bytes)(Pydantic v2). - Валідація - перевірка типів, обмежень (range, regex, custom validators) під час десеріалізації. У Pydantic відбувається одночасно з десеріалізацією.
Request body
from pydantic import BaseModel, Field
from fastapi import FastAPI
app = FastAPI()
class OrderIn(BaseModel):
customer_id: int
items: list[str] = Field(min_length=1)
note: str | None = None
@app.post("/orders")
async def create_order(order: OrderIn) -> dict:
# `order` is already validated. Invalid input never reaches here -
# FastAPI returns 422 with a per-field error list automatically.
return {"received": order.model_dump()}
response_model для виходу
class OrderOut(BaseModel):
id: int
customer_id: int
items: list[str]
# `internal_notes` intentionally absent from output schema
@app.post("/orders", response_model=OrderOut)
async def create_order(order: OrderIn) -> Order:
saved = await repo.insert(order)
return saved # SQLAlchemy Order with extra fields
response_model=OrderOut робить дві важливі речі:
- Фільтрація. Поля, відсутні у
OrderOut, не потраплять у відповідь - навіть якщоsavedмаєinternal_notes,password_hashчи інші чутливі атрибути. Захищає від витоку PII при випадковому додаванні полів у domain model. - Документація. OpenAPI-схема
/docsвідображає самеOrderOut, а не domain-модель.
Поведінкові опції: response_model_exclude_unset=True (повертати лише поля, які явно встановили на екземплярі моделі), response_model_exclude={"field"}, response_model_include={"field"}.
Антипатерн: domain model як response_model
Спокуса використовувати SQLAlchemy-модель або domain entity напряму як схему вводу-виводу спричиняє витоки полів і змішує транспортний контракт з доменом. Канонічний шлях - окремі Pydantic-схеми (OrderIn, OrderOut, OrderUpdate) і явне перетворення між ними і domain-об'єктами через model_validate (див. наступний розділ).
Links
model_validate у Pydantic v2⚑
Summary
model_validate(obj)- канонічний метод Pydantic v2 для побудови моделі з довільного об'єкта (dict, ORM entity, dataclass). Замінив v1-методиparse_objіfrom_orm. Опціяfrom_attributes=Trueуmodel_configдозволяє читати дані з атрибутів об'єкта (а не лише з dict-ключів).
v1 → v2 відповідність
| Pydantic v1 | Pydantic v2 |
|---|---|
Model.parse_obj(d) | Model.model_validate(d) |
Model.parse_raw(s) | Model.model_validate_json(s) |
Model.from_orm(obj) | Model.model_validate(obj) з from_attributes=True |
model.dict() | model.model_dump() |
model.json() | model.model_dump_json() |
Config.orm_mode = True | model_config = ConfigDict(from_attributes=True) |
Реалізація
from pydantic import BaseModel, ConfigDict
from sqlalchemy.orm import Session
class UserOut(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: str
full_name: str
def get_user(session: Session, user_id: int) -> UserOut:
user_row = session.get(User, user_id) # SQLAlchemy ORM object
return UserOut.model_validate(user_row)
Без from_attributes=True model_validate очікує dict-подібний об'єкт (__getitem__); з ним - читає через getattr, що покриває ORM-моделі, @dataclass, attrs-класи, NamedTuple.
Переваги над **user_row.__dict__
- Працює коректно з lazy-завантаженими relationship'ами SQLAlchemy: доступ через
getattrспрацьовує тригером загрузки (на відміну від__dict__, який лише дає immediate state). - Виконує валідацію типів - якщо домен-модель має
int, а схема очікуєstr, отримаємоValidationError, а не silent type mismatch. - Підтримує
field validatorsіmodel validatorsзі схеми.
Links
- Pydantic v2 migration guide - повний перелік перейменувань
- Pydantic docs:
model_validate
Розгортання FastAPI: Uvicorn, workers⚑
Summary
FastAPI запускається ASGI-сервером. Канонічний вибір - Uvicorn (опційно з
uvloopіhttptools). Для багатопроцесового деплою використовуютьuvicorn ... --workers Nабо gunicorn зUvicornWorker. Кожен worker - окремий процес з власним event loop'ом, не потік.
Запуск
# Local dev
uvicorn main:app --reload
# Production (single host, multiple workers)
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
# Production (gunicorn з UvicornWorker - кращий control над процесами)
gunicorn main:app -k uvicorn.workers.UvicornWorker -w 4 -b 0.0.0.0:8000
--reload і --workers взаємно виключні: reload-режим тримає лише один процес з watchdog'ом.
Worker = окремий процес
Кожен worker - окремий ОС-процес з власним:
- Event loop'ом (
asyncioабоuvloop). - Connection pool'ом до БД, кешу, MQ.
- Кешем у пам'яті процесу.
- Лічильниками метрик (якщо не централізовані).
Процеси не діляться пам'яттю - in-memory кеш у одному worker'і не видно іншим. Це наслідок Python GIL: щоб використати кілька CPU-ядер, потрібні окремі процеси, не потоки.
Кількість workers
Стандартна евристика для IO-bound (типовий FastAPI-API): 2 * cpu_count + 1 (відображає gunicorn-конвенцію). Для CPU-bound навантажень - близько до cpu_count. Реальна цифра підбирається бенчмарками з контролем p99 latency і CPU utilization.
Альтернативи Uvicorn
- Hypercorn - підтримує HTTP/2 і HTTP/3 нативно, на відміну від Uvicorn.
- Granian - Rust-based ASGI server, конкурує з Uvicorn по швидкості.
Links
- Uvicorn docs: Settings -
--workers,--loop,--http - FastAPI docs: Deployment
Тестування FastAPI: TestClient і dependency_overrides⚑
Summary
TestClient(зfastapi.testclient) - синхронний клієнт, який викликає ASGI-застосунок напряму без HTTP-сокета. Реалізований черезhttpx.app.dependency_overridesдозволяє підміняти будь-якуDepends-залежність у тестах без зміни production-коду.
TestClient
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_health() -> None:
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
TestClient запускає lifespan startup при вході в контекст-менеджер: with TestClient(app) as client: .... Без with lifespan не виконається, що часто плутає при ініціалізації global state. Канонічний шлях оголошення startup/shutdown - lifespan async context manager, переданий у FastAPI(lifespan=...); FastAPI 0.93.0 (2023-03-07) додав підтримку lifespan і позначив @app.on_event("startup"/"shutdown") як попередній підхід, що lifespan витісняє; у документації вони тепер у розділі "Alternative Events (deprecated)".
З FastAPI 0.87.0 (2022-11-13) TestClient побудований на httpx після апгрейду Starlette до 0.21.0 (раніше - на requests). API сумісне у більшості випадків, але є відмінності у тімаутах і обробці redirect'ів.
Async-aware тестування
Для тестів, які мають викликати async-функції напряму або перевіряти async-середовище більш контрольовано, використовують httpx.AsyncClient з ASGITransport:
import pytest
from httpx import ASGITransport, AsyncClient
from main import app
@pytest.mark.asyncio
async def test_create_order() -> None:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
response = await ac.post("/orders", json={"customer_id": 1, "items": ["a"]})
assert response.status_code == 201
Це обов'язковий шлях, якщо тест викликає async-fixture'и, які не підтримуються синхронним TestClient.
app.dependency_overrides
Канонічний механізм підміни залежностей у тестах:
from fastapi.testclient import TestClient
def fake_db():
return InMemoryDb()
def test_list_items() -> None:
app.dependency_overrides[get_db] = fake_db
try:
client = TestClient(app)
response = client.get("/items")
assert response.status_code == 200
finally:
app.dependency_overrides.clear()
Скидання після тесту обов'язкове - інакше override залишається активним для наступного тесту, який ділить той самий app. У pytest зручно через fixture з yield:
@pytest.fixture
def override_db():
app.dependency_overrides[get_db] = fake_db
yield
app.dependency_overrides.clear()
Підмінити можна будь-яку залежність на будь-якому рівні графа: верхньорівневу (get_db), вкладену (get_current_user, що сам залежить від get_db), guard з dependencies=[Depends(...)]. Це робить dependency_overrides основним інструментом для unit-тестування handler'ів без mock-бібліотек.
Links
- FastAPI docs: Testing
- FastAPI docs: Async Tests -
httpx.AsyncClientзASGITransport - FastAPI docs: Testing Dependencies with Overrides