Создание быстрых и надёжных API с помощью FastAPI

Раздел: Python -> Веб-фреймворки

Основы работы с FastAPI

FastAPI

– это современный веб-фреймворк для Python, который позволяет создавать высокопроизводительные API с минимальными усилиями. Основное преимущество – автоматическая генерация документации (OpenAPI) и строгая валидация данных на основе аннотаций типов.

Для начала работы достаточно установить пакет:

pip install fastapi[standard]

Fast api python (fastapi для python)

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

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

веб библиотека python (веб-библиотека python (фреймворк))

Запуск осуществляется командой:

uvicorn main:app --reload

Flask на языке python (flask на python)

После этого документация доступна по адресу /docs. Каждый маршрут автоматически добавляется в OpenAPI схему.

Типичная ошибка – забыть указать тип возвращаемого значения. Если аннотация отсутствует, FastAPI не сможет корректно сгенерировать схему ответа. Решение – всегда добавлять возвращаемый тип, даже если это dict.

Как сделать асинхронный эндпоинт для длительных операций?

Если внутри маршрута выполняются операции ввода-вывода (запросы к БД, внешние API), лучше использовать асинхронные функции:

@app.get("/async")
async def read_async():
    # имитация асинхронного вызова
    await some_async_task()
    return {"status": "done"}

Python manage py runserver (запуск сервера разработки django через manage.py runserver)

FastAPI поддерживает как синхронные, так и асинхронные обработчики. Для синхронных функций фреймворк запускает их в отдельном потоке, что может быть достаточно для небольших проектов. Асинхронные маршруты обрабатываются в одном потоке событий, что повышает производительность при большом количестве одновременных запросов.

Если внутри асинхронного маршрута вызвать блокирующую операцию (например, time.sleep), весь сервер зависнет. Используйте asyncio.sleep() или вынесите блокирующий код в функцию и запустите через asyncio.to_thread().

Как обрабатывать данные из тела запроса с валидацией?

Для этого используются Pydantic модели. Создаётся класс, унаследованный от BaseModel, с аннотированными полями:

from pydantic import BaseModel

class Item(BaseModel):
    name: str
    price: float
    is_offer: bool = False

@app.post("/items/")
def create_item(item: Item):
    return {"item_name": item.name, "item_price": item.price}

FastAPI автоматически валидирует входящие данные и возвращает понятную ошибку при несоответствии типов.

Проблема: если поле помечено как обязательное, но клиент не отправляет его, FastAPI вернёт HTTP 422 с детальным списком ошибок. Это ожидаемое поведение, но иногда требуется более мягкая валидация. Можно установить значение по умолчанию или сделать поле Optional.

Как организовать зависимости, например аутентификацию?

FastAPI предоставляет механизм Dependency Injection. Функция, помеченная Depends(), может быть вставлена в любой маршрут:

from fastapi import Depends

def verify_token(token: str = Header(...)):
    # проверка токена
    return token

@app.get("/protected")
async def protected_route(token: str = Depends(verify_token)):
    return {"token": token}

Это позволяет легко переиспользовать логику аутентификации, проверки прав доступа или подключения к базе данных.

Ошибка: если зависимость требует действий с побочными эффектами (например, открытие сессии БД), нужно следить за корректным закрытием ресурсов. Рекомендуется использовать генераторы с yield для создания зависимостей с очисткой.

Как обрабатывать ошибки и возвращать кастомные ответы?

Можно выбрасывать HTTPException с нужным кодом и деталями:

from fastapi import HTTPException

@app.get("/items/{item_id}")
def read_item(item_id: int):
    if item_id not in range(1, 100):
        raise HTTPException(status_code=404, detail="Item not found")
    return {"item_id": item_id}

Кроме того, можно определить глобальный обработчик исключений для кастомных ошибок.

Если не обработать исключение, FastAPI вернёт ошибку 500. Для отладки удобно включить debug=True при создании приложения, но в production это небезопасно.

Как добавить CORS middleware для работы с фронтендом?

Для разрешения кросс-доменных запросов используется CORSMiddleware:

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

В production стоит указывать конкретные домены, а не звёздочку.

Расширенные примеры использования FastAPI

Реализация WebSocket соединения

FastAPI поддерживает WebSocket. Пример эхо-сервера:

Пример
from fastapi import WebSocket

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
        data = await websocket.receive_text()
        await websocket.send_text(f"Message text was: {data}")

Клиент подключается через ws://localhost:8000/ws и отправляет сообщения. В ответ приходит то же сообщение с префиксом.

(Browser console)
> ws = new WebSocket("ws://localhost:8000/ws")
> ws.send("Привет")
> ws.onmessage = (e) => console.log(e.data)
< "Message text was: Привет"

WebSocket обработчик должен быть асинхронным. Если внутри цикла вызывается блокирующий код, соединение может разорваться.

Фоновая задача с использованием BackgroundTasks

Для выполнения операций после отправки ответа (например, отправка email) используется BackgroundTasks:

Пример
from fastapi import BackgroundTasks

def write_log(message: str):
    with open("log.txt", "a") as f:
        f.write(message + "\n")

@app.post("/send-notification")
async def send_notification(background_tasks: BackgroundTasks):
    background_tasks.add_task(write_log, "notification sent")
    return {"message": "Notification will be sent"}

Функция write_log выполняется в фоне после того, как ответ уже отправлен клиенту.

BackgroundTasks не подходят для тяжёлых длительных задач из-за блокировки event loop. Для таких задач лучше использовать Celery или RQ.

Загрузка файлов через multipart/form-data

FastAPI поддерживает загрузку файлов с помощью UploadFile:

Пример
from fastapi import File, UploadFile

@app.post("/uploadfile/")
async def create_upload_file(file: UploadFile = File(...)):
    contents = await file.read()
    # сохраняем файл
    with open(f"{file.filename}", "wb") as f:
        f.write(contents)
    return {"filename": file.filename, "size": len(contents)}

Клиент отправляет файл через форму. Размер файла можно получить до полного чтения, используя file.file.seek(0, 2) и file.file.tell().

POST /uploadfile/
Content-Type: multipart/form-data
...
Response: {"filename": "photo.jpg", "size": 123456}

При очень больших файлах чтение в память может быть неэффективным. Рекомендуется использовать потоковую запись: shutil.copyfileobj(file.file, output_file).

Пагинация через параметры запроса

Реализация пагинации с помощью Query и Pydantic:

Пример
from pydantic import BaseModel
from fastapi import Query

class Pagination(BaseModel):
    page: int = Query(1, ge=1)
    per_page: int = Query(10, ge=1, le=100)

@app.get("/items")
def list_items(pagination: Pagination = Depends()):
    # имитация базы данных
    items = list(range(1, 1001))
    start = (pagination.page - 1) * pagination.per_page
    end = start + pagination.per_page
    return {"page": pagination.page, "items": items[start:end]}
GET /items?page=2&per_page=5
Response: {"page": 2, "items": [6, 7, 8, 9, 10]}

Если не указать дефолтные значения для page и per_page, клиент обязан их передать. Лучше задать разумные значения по умолчанию.

Кэширование ответов с использованием Redis

Пример простого декоратора для кэширования:

Пример
import json
import aioredis
from functools import wraps

redis = aioredis.from_url("redis://localhost")

def cache(expire: int = 60):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            key = f"{func.__name__}:{json.dumps(kwargs)}"
            cached = await redis.get(key)
            if cached:
                return json.loads(cached)
            result = await func(*args, **kwargs)
            await redis.set(key, json.dumps(result), ex=expire)
            return result
        return wrapper
    return decorator

@app.get("/cached")
@cache(expire=30)
async def get_cached_data():
    # имитация дорогого запроса
    await asyncio.sleep(2)
    return {"data": "very expensive"}

Первый запрос выполняется 2 секунды, последующие (в течение 30 секунд) возвращают закэшированный ответ мгновенно.

First request: 2 sec delay
Second request (within 30 sec): instant response

Кэширование может привести к возврату устаревших данных. Следует осторожно выбирать время жизни и инвалидировать кэш при изменении данных.

FastAPI для Python - comments

En
Fast api python (python)