Материал учебного циклаОткрытый материал

FastAPI: как устроен современный Python API

Подробное введение в FastAPI: ASGI, Uvicorn, маршруты, параметры, Pydantic, валидация, зависимости, async, ошибки, middleware, фоновые задачи, тестирование и структура проекта.

Веб-разработка #API #ASGI #FastAPI #Pydantic #Python #REST #Uvicorn #backend
Учебный цикл Основы API и backend-разработки Материал 4 из 8

FastAPI: как устроен современный Python API

FastAPI - Python-фреймворк для создания HTTP API.

С его помощью можно написать сервер, который:

  • принимает запросы от сайта, мобильного приложения или другого сервиса;
  • проверяет входные данные;
  • выполняет программную логику;
  • обращается к базе данных и внешним API;
  • возвращает JSON;
  • автоматически создаёт документацию OpenAPI.

Минимальное приложение выглядит так:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {"message": "Hello, world"}

Здесь всего несколько строк, но за ними скрывается полный путь HTTP-запроса:

Клиент
-
сетевой сервер
-
маршрутизация
-
обработка параметров
-
функция Python
-
формирование HTTP-ответа

FastAPI не заменяет Python и не является отдельным сервером базы данных. Он связывает HTTP с обычными Python-функциями и типами.

HTTP-методы GET, POST, PUT, PATCH, DELETE и QUERY: примеры запросов


Какую задачу решает FastAPI

Обычная функция Python вызывается из другой части программы:

def calculate_total(price: float, quantity: int) -> float:
    return price * quantity


total = calculate_total(1200, 3)

Но браузер или внешний сервис не может напрямую вызвать эту функцию. Он умеет отправлять HTTP-запросы.

Нужно сопоставить:

HTTP-запрос
-
Python-функция

FastAPI создаёт такое сопоставление.

@app.get("/total")
def calculate_total(
    price: float,
    quantity: int,
) -> dict[str, float]:
    return {
        "total": price * quantity,
    }

Запрос:

GET /total?price=1200&quantity=3

Ответ:

{
  "total": 3600.0
}

Фреймворк выполняет несколько действий:

  1. Находит функцию, связанную с GET /total.
  2. Извлекает price и quantity из query-параметров.
  3. Преобразует значения из строк в float и int.
  4. Проверяет типы.
  5. Вызывает функцию.
  6. Преобразует словарь в JSON.
  7. Формирует HTTP-ответ.

Именно автоматическая связь между HTTP, аннотациями типов и Python-кодом является центральной идеей FastAPI.


FastAPI, Uvicorn, Starlette и Pydantic

FastAPI-приложение состоит не из одного компонента.

FastAPI

FastAPI предоставляет:

  • маршрутизацию;
  • описание параметров;
  • dependency injection;
  • интеграцию с OpenAPI;
  • обработку ошибок;
  • response models;
  • инструменты безопасности.

Starlette

FastAPI построен поверх Starlette.

Starlette предоставляет более низкоуровневые веб-возможности:

  • ASGI-приложение;
  • запросы и ответы;
  • middleware;
  • WebSocket;
  • фоновые задачи;
  • статические файлы;
  • тестовый клиент.

Обычно разработчик работает через интерфейс FastAPI, но многие объекты и механизмы приходят из Starlette.

Pydantic

Pydantic отвечает за работу с данными:

  • проверяет типы;
  • преобразует совместимые значения;
  • создаёт модели;
  • формирует понятные ошибки;
  • сериализует результат.

Например, строка "42" может быть преобразована в целое число, если поле объявлено как int.

Uvicorn

Uvicorn - ASGI-сервер.

Он:

  • открывает сетевой порт;
  • принимает HTTP-соединения;
  • преобразует входящие данные в ASGI-сообщения;
  • передаёт их FastAPI-приложению;
  • отправляет ответ клиенту.

Упрощённо:

Uvicorn - принимает сетевой запрос

FastAPI - решает, какая функция должна его обработать

FastAPI можно запускать не только через Uvicorn, но он является стандартным и распространённым вариантом.


Что такое ASGI

ASGI - интерфейс между Python-веб-приложением и сервером.

Он определяет, как сервер передаёт приложению:

  • HTTP-запросы;
  • WebSocket-соединения;
  • события запуска и остановки.

До ASGI широко использовался WSGI. Он хорошо подходит для классических синхронных приложений, но изначально не рассчитан на WebSocket и современную асинхронную модель.

ASGI позволяет приложению эффективно работать с большим количеством операций ожидания:

  • запросами к внешним API;
  • обращениями к асинхронной базе данных;
  • ожиданием сообщений;
  • WebSocket-соединениями;
  • потоковой передачей.

Важно:

ASGI не делает любой код быстрым автоматически. Он создаёт интерфейс, позволяющий использовать асинхронность и долгоживущие соединения.


Установка и запуск

Для нового проекта:

python -m venv .venv

Активация в Linux и macOS:

source .venv/bin/activate

В Windows PowerShell:

.venv\Scripts\Activate.ps1

Установка стандартного набора зависимостей:

pip install "fastapi[standard]"

Создадим main.py:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    return {
        "message": "FastAPI works",
    }

Запуск для разработки:

fastapi dev main.py

После запуска приложение обычно доступно по адресу:

http://127.0.0.1:8000

Интерактивная документация:

http://127.0.0.1:8000/docs

Альтернативное представление документации:

http://127.0.0.1:8000/redoc

Для production используется другой режим запуска и отдельная конфигурация окружения.


Объект приложения

app = FastAPI()

app - экземпляр FastAPI-приложения.

В нём регистрируются:

  • маршруты;
  • middleware;
  • обработчики ошибок;
  • зависимости;
  • события жизненного цикла;
  • настройки OpenAPI.

Сервер импортирует именно этот объект.

Запись:

main:app

означает:

main
-
модуль main.py

app
-
объект внутри модуля

Маршрут и path operation

Рассмотрим:

@app.get("/users")
async def get_users():
    return []

Декоратор

@app.get("/users")

Декоратор сообщает FastAPI:

HTTP-метод
-
GET

Путь
-
/users

Обработчик
-
get_users

Когда приходит GET /users, вызывается функция get_users().

Такая связка пути, метода и функции называется path operation.

Почему важен HTTP-метод

Один и тот же путь может поддерживать разные операции:

@app.get("/users")
async def get_users():
    return []


@app.post("/users")
async def create_user():
    return {"created": True}
GET /users - получить пользователей

POST /users - создать пользователя

FastAPI не заставляет строго следовать REST, но корректное использование методов делает API понятнее.


Параметры пути

Путь может содержать переменную:

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {
        "user_id": user_id,
    }

Запрос:

GET /users/42

FastAPI извлекает 42, преобразует в int и передаёт функции.

Если клиент отправит:

GET /users/abc

он получит ошибку валидации, потому что abc нельзя преобразовать в int.

Параметр пути подходит для идентификации конкретного ресурса:

/users/42
/orders/105
/products/phone-15

Query-параметры

Параметры функции, которых нет в пути, обычно интерпретируются как query-параметры.

@app.get("/products")
async def get_products(
    limit: int = 20,
    offset: int = 0,
    search: str | None = None,
):
    return {
        "limit": limit,
        "offset": offset,
        "search": search,
    }

Запрос:

GET /products?limit=10&offset=20&search=phone

Query-параметры удобно использовать для:

  • фильтрации;
  • поиска;
  • сортировки;
  • пагинации;
  • необязательных настроек.

Значение по умолчанию делает параметр необязательным:

limit: int = 20

Параметр без значения по умолчанию становится обязательным:

limit: int

Проверка и ограничения параметров

Тип int говорит, какое значение ожидается. Но часто нужны дополнительные ограничения.

from typing import Annotated

from fastapi import FastAPI, Query

app = FastAPI()


@app.get("/products")
async def get_products(
    limit: Annotated[
        int,
        Query(ge=1, le=100),
    ] = 20,
):
    return {
        "limit": limit,
    }

Ограничения:

ge=1 - значение не меньше 1

le=100 - значение не больше 100

Если клиент передаст limit=1000, FastAPI вернёт структурированную ошибку.

Annotated позволяет соединить Python-тип с дополнительной информацией FastAPI, не смешивая тип и значение по умолчанию.


Тело запроса и модели Pydantic

Для сложных данных используют JSON body.

Создадим модель:

from pydantic import BaseModel, Field


class ProductCreate(BaseModel):
    name: str = Field(min_length=2, max_length=100)
    price: float = Field(gt=0)
    quantity: int = Field(ge=0)
    description: str | None = None

Endpoint:

@app.post("/products")
async def create_product(
    product: ProductCreate,
):
    return {
        "product": product,
    }

Запрос:

POST /products
Content-Type: application/json
{
  "name": "Keyboard",
  "price": 4900,
  "quantity": 12
}

FastAPI понимает, что product является сложной Pydantic-моделью, поэтому данные нужно взять из тела запроса.

Что делает Pydantic

Pydantic:

  1. Читает JSON.
  2. Проверяет наличие обязательных полей.
  3. Проверяет типы.
  4. Применяет ограничения.
  5. Создаёт объект ProductCreate.
  6. Передаёт его функции.

Внутри endpoint доступен обычный Python-объект:

product.name
product.price
product.quantity

Ошибка валидации

Если клиент отправит:

{
  "name": "K",
  "price": -100,
  "quantity": -2
}

данные нарушают сразу несколько ограничений.

FastAPI вернёт ответ с кодом 422 и описанием ошибок.

Упрощённо:

{
  "detail": [
    {
      "loc": ["body", "name"],
      "msg": "String should have at least 2 characters"
    },
    {
      "loc": ["body", "price"],
      "msg": "Input should be greater than 0"
    }
  ]
}

Такой ответ полезен и клиенту, и разработчику: он показывает, где именно находится проблема.


Входная и выходная модели

Одну модель не всегда стоит использовать и для входа, и для ответа.

Например, клиент отправляет пароль, но API не должен возвращать его обратно.

class UserCreate(BaseModel):
    email: str
    password: str


class UserPublic(BaseModel):
    id: int
    email: str

Endpoint:

@app.post(
    "/users",
    response_model=UserPublic,
)
async def create_user(
    user: UserCreate,
):
    created_user = {
        "id": 101,
        "email": user.email,
        "password": user.password,
    }

    return created_user

Хотя функция вернула словарь с паролем, response_model=UserPublic ограничит ответ:

{
  "id": 101,
  "email": "user@example.com"
}

Response model:

  • проверяет выходные данные;
  • фильтрует лишние поля;
  • формирует схему документации;
  • фиксирует публичный контракт API.

Это не замена правильной работе с секретами, но важный дополнительный слой защиты.


HTTP-статусы и ошибки

Если пользователь не найден, нельзя возвращать обычный объект с текстом ошибки и статусом 200.

FastAPI предоставляет HTTPException.

from fastapi import HTTPException


@app.get("/users/{user_id}")
async def get_user(user_id: int):
    if user_id != 1:
        raise HTTPException(
            status_code=404,
            detail="User not found",
        )

    return {
        "id": 1,
        "name": "Alice",
    }

Ответ:

HTTP/1.1 404 Not Found
{
  "detail": "User not found"
}

Типичные статусы:

200 OK - успешное получение

201 Created - ресурс создан

204 No Content - успешно, но тело отсутствует

400 Bad Request - ошибка в логике запроса

401 Unauthorized - не выполнена аутентификация

403 Forbidden - доступ запрещён

404 Not Found - ресурс не найден

409 Conflict - конфликт состояния

422 Unprocessable Content - данные не прошли валидацию

500 Internal Server Error - необработанная ошибка сервера

Dependency injection

Во многих endpoint повторяются одинаковые действия:

  • получить подключение к базе;
  • проверить токен;
  • загрузить пользователя;
  • проверить права;
  • прочитать конфигурацию.

Копировать эту логику в каждую функцию неудобно.

FastAPI использует dependency injection.

from typing import Annotated

from fastapi import Depends, Header, HTTPException


def verify_api_key(
    x_api_key: Annotated[str, Header()],
) -> str:
    if x_api_key != "secret":
        raise HTTPException(
            status_code=401,
            detail="Invalid API key",
        )

    return x_api_key

Подключение зависимости:

@app.get("/private")
async def private_data(
    api_key: Annotated[
        str,
        Depends(verify_api_key),
    ],
):
    return {
        "access": True,
    }

Перед вызовом private_data() FastAPI:

  1. Вызывает verify_api_key().
  2. Извлекает заголовок.
  3. Проверяет ключ.
  4. Передаёт результат в endpoint.

Зависимость сама может иметь зависимости. Так создаётся дерево подготовки запроса.

Для чего применяют зависимости

  • аутентификация;
  • авторизация;
  • сессии базы данных;
  • общие query-параметры;
  • rate limiting;
  • конфигурация;
  • журналирование;
  • загрузка текущего пользователя.

Dependency injection помогает отделить инфраструктурную логику от основной задачи endpoint.


def или async def

FastAPI поддерживает оба варианта:

@app.get("/sync")
def sync_endpoint():
    return {"mode": "sync"}
@app.get("/async")
async def async_endpoint():
    return {"mode": "async"}

Когда полезен async def

Асинхронная функция особенно полезна, если она ждёт I/O:

  • HTTP-запрос;
  • асинхронную базу данных;
  • файл;
  • очередь;
  • таймер.
import httpx


@app.get("/external")
async def get_external_data():
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://example.com/api/data",
        )

    return response.json()

Во время await event loop может обслуживать другие запросы.

Когда async не помогает

Тяжёлое вычисление CPU:

def calculate_large_matrix():
    ...

не становится быстрее только из-за async def.

Если внутри асинхронного endpoint выполнить долгую синхронную операцию, она может заблокировать event loop.

Для CPU-bound задач используют:

  • отдельный процесс;
  • очередь задач;
  • worker;
  • специализированный вычислительный сервис.

Практическое правило

  • Асинхронная библиотека - используйте async def и await.
  • Синхронная библиотека - обычный def часто безопаснее.
  • Не вызывайте долгий блокирующий код напрямую внутри async def.

FastAPI умеет выполнять обычные def-обработчики в thread pool, чтобы они не блокировали основной event loop.


Middleware

Middleware оборачивает обработку каждого запроса.

Запрос
↓
middleware
↓
endpoint
↓
middleware
↓
ответ

Пример измерения времени:

import time

from fastapi import Request


@app.middleware("http")
async def add_process_time(
    request: Request,
    call_next,
):
    started_at = time.perf_counter()

    response = await call_next(request)

    process_time = (
        time.perf_counter() - started_at
    )

    response.headers["X-Process-Time"] = str(
        process_time
    )

    return response

Middleware подходит для сквозных задач:

  • логирование;
  • CORS;
  • измерение времени;
  • трассировка;
  • общие заголовки;
  • обработка request ID.

Не стоит переносить в middleware всю бизнес-логику. Его задача - обработка, общая для множества маршрутов.


CORS

Браузер применяет политику same-origin.

Если frontend работает на:

https://app.example.com

а API на:

https://api.example.com

браузер считает их разными origin.

Для разрешения запросов настраивается CORS middleware.

from fastapi.middleware.cors import CORSMiddleware


app.add_middleware(
    CORSMiddleware,
    allow_origins=[
        "https://app.example.com",
    ],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

CORS - механизм браузера. Он не является полноценной системой аутентификации и не защищает API от запросов, отправленных не из браузера.

Preflight - предварительный запрос OPTIONS, которым браузер проверяет, разрешён ли будущий запрос.


Фоновые задачи

Небольшую операцию можно запустить после формирования ответа.

from fastapi import BackgroundTasks


def send_email(
    email: str,
    message: str,
) -> None:
    print("Sending email:", email, message)


@app.post("/notifications")
async def create_notification(
    email: str,
    background_tasks: BackgroundTasks,
):
    background_tasks.add_task(
        send_email,
        email,
        "Notification created",
    )

    return {
        "accepted": True,
    }

Клиент получает ответ, а задача выполняется после него.

BackgroundTasks подходят для небольших локальных действий:

  • отправить письмо;
  • записать лог;
  • обработать небольшой файл.

Они не заменяют полноценную очередь.

Если задача:

  • длительная;
  • критически важная;
  • должна пережить перезапуск;
  • требует повторных попыток;
  • выполняется на другом сервере,

лучше использовать отдельную систему фоновых задач.


Жизненный цикл приложения

Приложению может потребоваться создать ресурс при запуске и закрыть его при остановке:

  • пул соединений;
  • HTTP-клиент;
  • модель машинного обучения;
  • кэш;
  • очередь.

Для этого используют lifespan.

from contextlib import asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    print("Application started")

    yield

    print("Application stopped")


app = FastAPI(lifespan=lifespan)

Код до yield выполняется при запуске, после yield - при завершении.


Структура проекта

Один main.py подходит для обучения, но крупное приложение лучше разделить.

app/
├── __init__.py
├── main.py
├── api/
│   ├── __init__.py
│   ├── users.py
│   └── products.py
├── schemas/
│   ├── user.py
│   └── product.py
├── services/
│   ├── users.py
│   └── payments.py
├── repositories/
│   └── users.py
├── dependencies/
│   └── auth.py
├── core/
│   ├── config.py
│   └── security.py
└── db/
    └── session.py

Это не единственная правильная структура.

Полезное разделение:

routers - HTTP-слой

schemas - форматы входа и выхода

services - бизнес-логика

repositories - работа с хранилищем

dependencies - повторно используемая подготовка запроса

Endpoint не должен превращаться в функцию на несколько сотен строк.


APIRouter

Маршруты можно вынести в отдельный модуль.

app/api/users.py:

from fastapi import APIRouter

router = APIRouter(
    prefix="/users",
    tags=["users"],
)


@router.get("/")
async def get_users():
    return []


@router.get("/{user_id}")
async def get_user(user_id: int):
    return {
        "id": user_id,
    }

Подключение в main.py:

from fastapi import FastAPI

from app.api.users import router as users_router


app = FastAPI()

app.include_router(users_router)

APIRouter помогает группировать:

  • пути;
  • tags;
  • зависимости;
  • префиксы;
  • response models.

OpenAPI и автоматическая документация

FastAPI строит OpenAPI-схему на основании:

  • маршрутов;
  • HTTP-методов;
  • параметров;
  • моделей Pydantic;
  • response models;
  • статусов;
  • описаний.

Схема обычно доступна по адресу:

/openapi.json

На её основе работают:

/docs - Swagger UI

/redoc - ReDoc

Документация не является просто красивой страницей. OpenAPI можно использовать для:

  • генерации клиента;
  • тестирования;
  • согласования контракта с frontend;
  • интеграции с API gateway;
  • проверки изменений схемы.

Описание endpoint

@app.post(
    "/products",
    response_model=ProductPublic,
    status_code=201,
    summary="Create product",
    tags=["products"],
)
async def create_product(
    product: ProductCreate,
):
    ...

Эти данные попадут в OpenAPI.


Работа с базой данных

FastAPI не содержит встроенной ORM.

Можно использовать:

  • SQLAlchemy;
  • SQLModel;
  • asyncpg;
  • psycopg;
  • другие библиотеки.

Это важное архитектурное разделение:

FastAPI - HTTP-слой

ORM или драйвер - работа с базой данных

Типовой endpoint не должен сам создавать новое соединение без управления жизненным циклом.

Подключение часто передают через dependency:

def get_session():
    session = create_session()

    try:
        yield session
    finally:
        session.close()

FastAPI выполняет код до yield, передаёт ресурс endpoint, а затем выполняет очистку.


Аутентификация и авторизация

Эти понятия нельзя смешивать.

Аутентификация

Отвечает на вопрос:

Кто выполняет запрос?

Примеры:

  • API key;
  • логин и пароль;
  • session cookie;
  • JWT;
  • OAuth2.

Авторизация

Отвечает:

Что этому пользователю разрешено?

Например:

обычный пользователь - читает собственный профиль

администратор - просматривает всех пользователей

FastAPI предоставляет инструменты описания security schemes и интеграцию с OpenAPI, но правила доступа всё равно проектирует разработчик.


Тестирование

FastAPI-приложение можно тестировать без запуска отдельного внешнего сервера.

from fastapi.testclient import TestClient

from app.main import app


client = TestClient(app)


def test_root() -> None:
    response = client.get("/")

    assert response.status_code == 200
    assert response.json() == {
        "message": "Hello, world",
    }

Тест может проверять:

  • статус;
  • JSON;
  • заголовки;
  • авторизацию;
  • ошибки валидации;
  • зависимости;
  • работу маршрута.

Подмена зависимости

Dependency injection упрощает тесты.

Например, реальную базу можно заменить тестовой зависимостью:

app.dependency_overrides[get_session] = (
    get_test_session
)

После теста overrides нужно очистить.


Обработка исключений

Можно зарегистрировать собственный обработчик.

from fastapi import Request
from fastapi.responses import JSONResponse


class DomainError(Exception):
    def __init__(self, message: str):
        self.message = message


@app.exception_handler(DomainError)
async def domain_error_handler(
    request: Request,
    exc: DomainError,
):
    return JSONResponse(
        status_code=409,
        content={
            "error": "domain_error",
            "message": exc.message,
        },
    )

Это помогает создать единый формат ошибок.

Но не следует показывать клиенту внутренний traceback, SQL-запросы или секретные данные.


Что происходит при одном запросе

Соберём полный путь.

Запрос:

POST /products?notify=true
Authorization: Bearer ...
Content-Type: application/json
{
  "name": "Keyboard",
  "price": 4900,
  "quantity": 12
}

Последовательность:

1. Uvicorn принимает соединение.
2. ASGI передаёт запрос приложению.
3. Middleware выполняет общую обработку.
4. FastAPI находит POST /products.
5. Dependencies проверяют токен.
6. Query-параметр notify преобразуется в bool.
7. JSON проверяется моделью ProductCreate.
8. Вызывается endpoint.
9. Service выполняет бизнес-логику.
10. Repository сохраняет данные.
11. Response model фильтрует ответ.
12. FastAPI сериализует JSON.
13. Middleware обрабатывает ответ.
14. Uvicorn отправляет его клиенту.

Когда эта последовательность понятна, FastAPI перестаёт выглядеть как набор магических декораторов.


Типичные ошибки

Вся логика находится в endpoint

@app.post("/orders")
async def create_order(...):
    # 300 строк

Лучше оставить в HTTP-слое получение данных и формирование ответа, а бизнес-логику вынести.

Pydantic-модели смешаны с ORM-моделями

Формат API и структура базы решают разные задачи. Иногда их можно связать, но не нужно автоматически считать одним и тем же объектом.

Секреты находятся в коде

API_KEY = "real-secret"

Секреты хранят в переменных окружения или специализированном хранилище.

Блокирующий код вызывается внутри async def

Это может остановить обработку других запросов текущим event loop.

Ошибки всегда возвращаются с 200

HTTP-статус является частью контракта.

Пароль возвращается в response

Используйте отдельные входные и выходные модели.

Нет таймаутов внешних запросов

Обращение к другому сервису может зависнуть и занять все рабочие ресурсы.

Создаётся новый клиент или пул на каждый запрос

Долгоживущие ресурсы лучше создавать на lifespan и переиспользовать.

Документация считается защитой

Скрытие /docs не заменяет аутентификацию.


FastAPI и вебхуки

FastAPI хорошо подходит для приёма webhook.

@app.post("/webhooks/payment")
async def payment_webhook(
    request: Request,
):
    raw_body = await request.body()

    # Проверка подписи по raw_body
    # Проверка event_id
    # Сохранение события

    return {
        "received": True,
    }

Но FastAPI решает только HTTP-часть.

Для надёжной webhook-интеграции отдельно нужны:

  • проверка подписи;
  • идемпотентность;
  • хранение event ID;
  • быстрый ответ;
  • фоновые задачи или очередь;
  • retry;
  • логирование.

Что такое webhook и как он работает


FastAPI и машинное обучение

Модель машинного обучения можно предоставить через HTTP API.

Клиент
↓
POST /predict
↓
FastAPI
↓
подготовка признаков
↓
ML-модель
↓
прогноз

Пример:

class PredictionRequest(BaseModel):
    age: int
    income: float
    purchases: int


class PredictionResponse(BaseModel):
    probability: float
    predicted_class: int


@app.post(
    "/predict",
    response_model=PredictionResponse,
)
async def predict(
    data: PredictionRequest,
):
    probability = 0.82

    return PredictionResponse(
        probability=probability,
        predicted_class=int(
            probability >= 0.5
        ),
    )

Модель лучше загрузить один раз при старте приложения, а не читать с диска на каждый запрос.

FastAPI не обучает ML-модель. Он предоставляет сетевой интерфейс к уже написанной логике.


Разработка и production

Команда разработки обычно включает автоматическую перезагрузку:

fastapi dev

Production-среда отличается:

  • отключена автоматическая перезагрузка;
  • настроены HTTPS и reverse proxy;
  • определено число процессов;
  • включено логирование;
  • есть мониторинг;
  • заданы таймауты;
  • секреты приходят из окружения;
  • выполняются миграции базы;
  • настроена корректная остановка;
  • используются health checks.

Нельзя просто запустить development server и считать систему готовой к нагрузке.

Несколько процессов

Один процесс Python использует собственную память.

Если запустить несколько workers:

worker 1
worker 2
worker 3

каждый загрузит собственный экземпляр:

  • кэша;
  • глобальных переменных;
  • ML-модели;
  • соединений.

Это важно учитывать при проектировании памяти и состояния.

Глобальный set или словарь не является общей базой для нескольких workers.


Когда FastAPI подходит

FastAPI хорошо подходит для:

  • REST API;
  • webhook endpoint;
  • backend мобильного приложения;
  • внутренних микросервисов;
  • ML inference API;
  • интеграционного слоя;
  • сервисов автоматизации;
  • WebSocket-приложений;
  • административного backend.

Когда нужен другой инструмент

FastAPI не обязательно является лучшим выбором, если:

  • нужен большой серверный HTML-проект с готовой админкой;
  • требуется только простой одноразовый скрипт;
  • задача решается serverless-функцией;
  • команда использует другую зрелую платформу;
  • основная нагрузка является тяжёлым CPU-вычислением и HTTP-слой вторичен.

Фреймворк выбирают под архитектуру, команду и требования, а не только по скорости написания первого endpoint.


Минимальный проект целиком

main.py:

from typing import Annotated

from fastapi import (
    Depends,
    FastAPI,
    HTTPException,
    Query,
)
from pydantic import BaseModel, Field


app = FastAPI(
    title="Products API",
    version="1.0.0",
)


class ProductCreate(BaseModel):
    name: str = Field(
        min_length=2,
        max_length=100,
    )
    price: float = Field(gt=0)
    quantity: int = Field(ge=0)


class ProductPublic(ProductCreate):
    id: int


products: dict[int, ProductPublic] = {}


def get_next_id() -> int:
    return len(products) + 1


@app.post(
    "/products",
    response_model=ProductPublic,
    status_code=201,
)
async def create_product(
    product: ProductCreate,
    product_id: Annotated[
        int,
        Depends(get_next_id),
    ],
):
    created = ProductPublic(
        id=product_id,
        **product.model_dump(),
    )

    products[product_id] = created

    return created


@app.get(
    "/products",
    response_model=list[ProductPublic],
)
async def get_products(
    limit: Annotated[
        int,
        Query(ge=1, le=100),
    ] = 20,
):
    return list(products.values())[:limit]


@app.get(
    "/products/{product_id}",
    response_model=ProductPublic,
)
async def get_product(
    product_id: int,
):
    product = products.get(product_id)

    if product is None:
        raise HTTPException(
            status_code=404,
            detail="Product not found",
        )

    return product

Этот пример показывает:

  • объект приложения;
  • path operations;
  • GET и POST;
  • path и query-параметры;
  • модели Pydantic;
  • response model;
  • dependency;
  • HTTP-ошибку;
  • автоматическую документацию.

Словарь используется только для демонстрации. После перезапуска данные исчезнут, а несколько workers не будут видеть общее состояние. В реальном проекте понадобится база данных.


Итог

FastAPI связывает HTTP-запросы с Python-функциями и аннотациями типов.

Основной путь:

HTTP-запрос
-
ASGI-сервер
-
маршрут FastAPI
-
зависимости
-
валидация Pydantic
-
Python-функция
-
response model
-
JSON-ответ

FastAPI отвечает за HTTP-слой:

  • маршрутизацию;
  • параметры;
  • валидацию;
  • зависимости;
  • документацию;
  • обработку ошибок;
  • формирование ответа.

Uvicorn принимает сетевые соединения. Starlette предоставляет ASGI-фундамент. Pydantic проверяет и сериализует данные.

Для простого приложения достаточно нескольких функций. Для серьёзной системы дополнительно нужны:

  • архитектурное разделение;
  • база данных;
  • аутентификация и авторизация;
  • тесты;
  • таймауты;
  • логирование;
  • мониторинг;
  • корректное развёртывание;
  • обработка фоновых задач.

Главная ценность FastAPI не в том, что он полностью скрывает устройство backend. Наоборот, он позволяет достаточно явно описать контракт API средствами Python:

путь
+
HTTP-метод
+
типы
+
модели данных
+
зависимости
+
формат ответа

Когда понятен путь одного запроса от Uvicorn до функции и обратно, остальные возможности FastAPI складываются в единую систему.

Продолжить

  1. 01 Django: как устроен Python-фреймворк для веб-приложений Разбираем Django с нуля: проект и приложения, маршрутизацию, представления, модели, ORM, шаблоны, формы, middleware, админ-панель и путь HTTP-запроса.
  2. 02 HTTP-методы GET, POST, PUT, PATCH, DELETE и QUERY: как работают запросы Подробно разбираем HTTP-методы GET, POST, PUT, PATCH, DELETE и новый QUERY: назначение, примеры запросов, URL, headers, body, безопасность и идемпотентность.
  3. 03 Как подключить внешний API к программе: методы, параметры, заголовки и тело запроса Разбираем, как подключить внешний API к Python-программе или автоматизации: найти endpoint, выбрать HTTP-метод, передать параметры, заголовки и JSON, прочитать ответ и обработать ошибки.
  4. 04 Что такое webhook и как сервисы сообщают друг другу о событиях Подробно разбираем принцип работы вебхуков, отличие от polling и обычного API-запроса, устройство webhook-запроса, ответы, повторную доставку, идемпотентность, безопасность и обработку ошибок.

Обратная связь

Материал был полезен?

Нашли ошибку или неточность? Сообщить →