FastAPI - это относительно молодой фреймворк для создания веб-API, который за короткое время заработал бешеную популярность в Python-сообществе. И не зря. Я помню, как впервые запустил приложение на FastAPI и мои коллеги не поверили, что весь бэкенд был написан всего за пару часов.
FastAPI выделяется сверхвысокой производительностью - он работает на базе Starlette и Pydantic, что делает его одним из самых быстрых Python-фреймворков. На тестах он обходит даже Node.js и Go по некоторым метрикам! Но скорость - лишь часть истории. Когда я сравнивал FastAPI с Flask, с которым работал раньше, меня поразила встроенная поддержка асинхронного программирования. В Flask для этого нужны дополнительные библиотеки и нетривиальные настройки, а тут все работает из коробки. А по сравнению с Django, FastAPI не таскает за собой тонны лишнего функционала, когда вам нужен просто легкий API. Самой восхитительной особенностью, по моему опыту, стала автоматическая генерация документации. Вы пишете код, а FastAPI сам создает интерактивный Swagger UI и ReDoc. Больше никаких отдельных документов, которые устаревают на следущий день после написания!
Встроенная валидация данных через Pydantic - еще один козырь в рукаве. Я перестал писать десятки строк кода для проверки входных данных. Просто определяете модель с типами полей, и фреймворк сам валидирует запросы и выдает понятные ошибки.
Недостатки? Ну, если вам нужен монолитный сайт с админкой, шаблонами и всем прочим - Django по-прежнему король. Если вы предпочитаете максимальную гибкость и простоту - Flask останется вашим выбором. Но для большинства современных API, особенно если вы работаете с микросервисами, FastAPI - это именно то, что доктор прописал.
Практическая часть: Создаем минимальный API
Хватит теории! Давайте напишем реальный код и посмотрим, как FastAPI работает на практике. Для начала нам нужно настроить окружение. Я всегда рекомендую использовать виртуальное окружение, чтобы избежать конфликтов между пакетами разных проектов. Начнем с создания папки для проекта и активации виртуального окружения:
| Bash | 1
2
3
4
| mkdir fastapi_app
cd fastapi_app
python -m venv venv
source venv/bin/activate # На Windows: venv\Scripts\activate |
|
Теперь установим необходимые пакеты - нам понадобятся сам FastAPI и сервер Uvicorn для запуска приложения:
| Bash | 1
| pip install fastapi uvicorn |
|
Uvicorn - это ASGI-сервер, который необходим для запуска асинхронных приложений. Он намного быстрее чем традиционные WSGI-серверы, которые использует Flask.
Что касается IDE, я обычно использую VS Code или PyCharm. Для VS Code рекомендую установить расширения Python, Pylance и даже есть специальное расширение FastAPI Snippets, которое добавляет полезные шаблоны кода. Если вы, как и я, забываете синтаксис - такие сниппеты неоценимы.
Создадим базовую структуру проекта:
| Python | 1
2
3
| fastapi_app/
├── venv/ # Виртуальное окружение
├── main.py # Главный файл приложения |
|
Теперь откроем main.py и напишем наш первый FastAPI-эндпоинт:
| Python | 1
2
3
4
5
6
7
| from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Привет, FastAPI!"} |
|
Этот код создает экземпляр приложения FastAPI и определяет маршрут для корневого URL (/), который возвращает JSON с приветствием. Обратите внимание на декоратор @app.get("/") - это говорит FastAPI, что функция read_root() должна обрабатывать GET-запросы к корневому пути.
Запустим наше приложение:
| Bash | 1
| uvicorn main:app --reload |
|
Здесь main - это имя файла (main.py), app - экземпляр FastAPI, а флаг --reload позволяет серверу автоматически перезагружаться при изменении кода, что очень удобно во время разработки. После запуска, перейдите по адресу http://127.0.0.1:8000 в браузере, и вы увидите JSON-ответ: {"message": "Привет, FastAPI!"}. Не впечатляет? Подождите, это только начало!
Давайте создадим более полезный API для управления списком продуктов. Допустим, мы хотим иметь возможность добавлять продукты и получать их по ID.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| from fastapi import FastAPI, HTTPException
app = FastAPI()
# Имитация базы данных
products = []
@app.post("/products/")
def create_product(name: str, price: float):
product = {"id": len(products), "name": name, "price": price}
products.append(product)
return product
@app.get("/products/{product_id}")
def read_product(product_id: int):
if product_id < 0 or product_id >= len(products):
raise HTTPException(status_code=404, detail="Продукт не найден")
return products[product_id] |
|
В этом примере я добавил два эндпоинта:
1. POST /products/ - принимает имя и цену продукта через параметры запроса и добавляет его в список.
2. GET /products/{product_id} - возвращает продукт по его ID.
Обратите внимание, как просто я определил типы для параметров функций. FastAPI автоматически валидирует их и преобразует в нужный тип. Если кто-то отправит product_id="не_число", FastAPI сам вернет ошибку с понятным сообщением.
Для проверки API вы можете использовать curl или любой другой HTTP-клиент:
| Bash | 1
2
3
4
5
| # Добавляем продукт
curl -X POST "http://127.0.0.1:8000/products/?name=Яблоко&price=1.5"
# Получаем продукт по ID
curl "http://127.0.0.1:8000/products/0" |
|
Но есть способ проще. Одна из фишек FastAPI - автоматическая генерация интерактивной документации. Перейдите по адресу http://127.0.0.1:8000/docs, и вы увидите Swagger UI с описанием всех эндпоинтов. Вы можете тестировать их прямо из браузера! Также доступна альтернативная документация ReDoc по адресу http://127.0.0.1:8000/redoc - она менее интерактивна, но более наглядна и удобна для чтения.
Теперь давайте использовать Pydantic для валидации данных. В предыдущем примере мы просто принимали параметры запроса. Но часто нужно обрабатывать более сложные данные, отправленные в теле запроса.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
class Product(BaseModel):
name: str
price: float
description: str = None # Опциональное поле
app = FastAPI()
products = []
@app.post("/products/")
def create_product(product: Product):
new_product = product.dict()
new_product["id"] = len(products)
products.append(new_product)
return new_product
@app.get("/products/{product_id}")
def read_product(product_id: int):
if product_id < 0 or product_id >= len(products):
raise HTTPException(status_code=404, detail="Продукт не найден")
return products[product_id] |
|
Я создал класс Product, унаследованный от BaseModel. Это Pydantic-модель, которая определяет структуру данных продукта и их типы. Поле description опциональное, поэтому я установил для него значение по умолчанию None. Теперь FastAPI будет ожидать, что запрос к /products/ будет содержать JSON в теле с полями name, price и, возможно, description. Если какие-то поля отсутствуют или имеют неверный тип, пользователь получит ошибку с детальным описанием проблемы. Чтобы протестировать это, отправьте POST-запрос с JSON-телом:
| Bash | 1
2
3
| curl -X POST "http://127.0.0.1:8000/products/" \
-H "Content-Type: application/json" \
-d '{"name": "Груша", "price": 2.0, "description": "Сочная груша"}' |
|
А теперь проверте автоматически сгенерированную документацию - вы увидите, что Swagger UI теперь показывает структуру ожидаемого JSON и даже позволяет заполнить форму для тестирования. Мне потребовалось всего несколько строк кода, чтобы создать API с валидацией данных и автоматической документацией. Я не могу передать, насколько это упрощает разработку по сравнению с написанием всей этой логики вручную.
Теперь попробуем улучшить наш API, добавив больше функциональности и гибкости. В реальных проектах часто требуется получать список элементов с фильтрацией и сортировкой. Давайте реализуем это:
| Python | 1
2
3
4
5
6
7
8
9
10
| @app.get("/products/")
def list_products(skip: int = 0, limit: int = 10, min_price: float = None):
result = products
# Фильтрация по минимальной цене
if min_price is not None:
result = [p for p in result if p["price"] >= min_price]
# Пагинация
return result[skip : skip + limit] |
|
В этом примере я добавил эндпоинт для получения списка продуктов с возможностью пагинации (skip и limit) и фильтрации по минимальной цене. Параметры запроса с значениями по умолчанию становятся опциональными – пользователь может их не указывать.
Я обожаю этот подход FastAPI к параметрам! Когда я использовал Flask, приходилось писать кучу дополнительного кода для извлечения и валидации параметров запроса. А здесь всё происходит автоматически. Кстати, можно также указывать ограничения на параметры с помощью специальных валидаторов. Например, давайте убедимся, что количество запрашиваемых продуктов не превышает 100, а skip не отрицательный:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from fastapi import FastAPI, HTTPException, Query, Path
@app.get("/products/")
def list_products(
skip: int = Query(0, ge=0, description="Сколько продуктов пропустить"),
limit: int = Query(10, ge=1, le=100, description="Максимальное количество продуктов"),
min_price: float = Query(None, description="Минимальная цена для фильтрации")
):
result = products
if min_price is not None:
result = [p for p in result if p["price"] >= min_price]
return result[skip : skip + limit]
@app.get("/products/{product_id}")
def read_product(product_id: int = Path(..., ge=0, description="ID продукта")):
if product_id >= len(products):
raise HTTPException(status_code=404, detail="Продукт не найден")
return products[product_id] |
|
Обратите внимание на использование Query и Path. Эти функции позволяют задавать дополнительные ограничения и метаданные для параметров. ge=0 означает "greater than or equal to 0" (больше или равно 0), а le=100 - "less than or equal to 100" (меньше или равно 100). Многоточие ... в Path(..., ge=0) означает, что параметр обязательный (без значения по умолчанию). Я наконец-то нашел фреймворк, где валидация данных не превращается в кошмар из десятков условий if-else! Эти метаданные также отображаются в автоматической документации, делая её еще полезнее.
Теперь давайте улучшим наш API для создания продуктов, добавив возможность обновления и удаления:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| @app.put("/products/{product_id}")
def update_product(product_id: int, product: Product):
if product_id < 0 or product_id >= len(products):
raise HTTPException(status_code=404, detail="Продукт не найден")
updated_product = product.dict()
updated_product["id"] = product_id
products[product_id] = updated_product
return updated_product
@app.delete("/products/{product_id}")
def delete_product(product_id: int):
if product_id < 0 or product_id >= len(products):
raise HTTPException(status_code=404, detail="Продукт не найден")
deleted_product = products.pop(product_id)
# Обновляем ID оставшихся продуктов
for i, p in enumerate(products):
p["id"] = i
return {"message": f"Продукт '{deleted_product['name']}' удален"} |
|
Теперь у нас полноценный CRUD API для работы с продуктами. Хотя в этом примере я использую простой список вместо базы данных, в реальном проекте вы подключите ORM вроде SQLAlchemy или асинхронный драйвер для MongoDB.
Не могу не отметить, насколько элегантно FastAPI интегрируется с Pydantic. Модели Pydantic не только валидируют входящие данные, но и помогают определить структуру ответа API. Давайте улучшим наш код, определив модель ответа:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| from typing import List, Optional
from pydantic import BaseModel, Field
class ProductBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100, description="Название продукта")
price: float = Field(..., gt=0, description="Цена продукта в рублях")
description: Optional[str] = Field(None, max_length=1000, description="Описание продукта")
class ProductCreate(ProductBase):
pass
class ProductResponse(ProductBase):
id: int = Field(..., description="Уникальный идентификатор продукта")
@app.post("/products/", response_model=ProductResponse)
def create_product(product: ProductCreate):
new_product = product.dict()
new_product["id"] = len(products)
products.append(new_product)
return new_product
@app.get("/products/{product_id}", response_model=ProductResponse)
def read_product(product_id: int = Path(..., ge=0)):
if product_id >= len(products):
raise HTTPException(status_code=404, detail="Продукт не найден")
return products[product_id]
@app.get("/products/", response_model=List[ProductResponse])
def list_products(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
min_price: float = Query(None, ge=0)
):
result = products
if min_price is not None:
result = [p for p in result if p["price"] >= min_price]
return result[skip : skip + limit] |
|
Здесь я создал базовую модель ProductBase с общими полями, от которой наследуются модели для создания продукта и для ответа API. Это хороший паттерн для разделения входных и выходных данных. Параметр response_model в декораторах указывает FastAPI, какую структуру должен иметь ответ.
Обратите внимание на использование Field – это аналог Query и Path, но для полей Pydantic-моделей. С его помощью можно задавать валидацию, значения по умолчанию и документацию для полей модели.
Теперь давайте глубже изучим возможности автоматической документации. FastAPI генерирует OpenAPI-схему, которая включает все пути, параметры, типы данных и даже примеры запросов и ответов. Вы можете настроить глобальные метаданные для своего API:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| app = FastAPI(
title="API для управления продуктами",
description="Этот API позволяет управлять каталогом продуктов",
version="0.1.0",
terms_of_service="http://example.com/terms/",
contact={
"name": "Иван Иванов",
"url": "http://example.com/contact/",
"email": "ivan@example.com",
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT",
},
) |
|
Эти метаданные отображаются в документации и помогают пользователям понять, для чего предназначен ваш API и как с вами связаться в случае проблем. Я также часто добавляю примеры для запросов и ответов, чтобы сделать документацию еще понятнее:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from fastapi import Body
@app.post("/products/", response_model=ProductResponse)
def create_product(
product: ProductCreate = Body(
...,
example={
"name": "Яблоко",
"price": 1.5,
"description": "Свежее красное яблоко"
}
)
):
new_product = product.dict()
new_product["id"] = len(products)
products.append(new_product)
return new_product |
|
Параметр example определяет пример данных, который будет отображаться в Swagger UI. Это особено полезно для сложных моделей с множеством полей.
Python FastAPI не отображается html страница Добрый день! Подскажите, пожалуйста, почему у меня не отображается html файл, хотя всё работает без... Разработка на fastapi с jinja + uvicorn + starlette -- это какая архитектра?) Добрый вечер, сразу прошу прощение за возможно глупый и банальный вопрос. Если я разрабатываю... Сколько оперативной памяти сервера нужно питону и среде для запуска fastapi? Доброй ночи всем, кто с радостью набирает код на Питоне!
Я пока не определился в пользу выбора... FastAPI и сериалайзеры В моём проекте три таблицы. Нужно сформировать ответ который дергает инфу сразу из всех трёх...
Углубляемся в детали
В своем первом проекте на FastAPI я поначалу использовал всего 10% его возможностей, и уже это показалось мне революционным прорывом. Но когда я узнал про остальные 90% - вот тогда наступило настоящее просветление.
Pydantic: больше чем просто валидация
Мы уже затронули Pydantic-модели, но они способны на гораздо большее, чем простая валидация полей. Pydantic - это настоящий фундамент FastAPI, который решает множество болезненных проблем разработки.
Во-первых, давайте посмотрим на более сложные примеры валидации:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| from pydantic import BaseModel, Field, validator
from typing import List, Optional
from datetime import date
class Category(BaseModel):
name: str
description: Optional[str] = None
class ProductDetailed(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0)
is_in_stock: bool = True
tags: List[str] = []
manufactured: date
category: Category
@validator('manufactured')
def check_date(cls, v):
if v > date.today():
raise ValueError("Дата производства не может быть в будущем")
return v
@validator('tags')
def check_tags(cls, v):
if len(v) > 5:
raise ValueError("Слишком много тегов (максимум 5)")
return v |
|
Здесь я использую валидаторы для проверки пользовательской логики. Например, дата производства не может быть в будущем, а количество тегов ограничено пятью. Метод validator - это декоратор, который позволяет задать функцию для валидации конкретного поля. Но самое интересное - это вложенные модели. Обратите внимание на поле category типа Category. Pydantic рекурсивно проверит все поля вложенной модели, и если что-то не так, выдаст детальную ошибку с указанием точного пути к проблемному полю. Я перестал писать однообразные проверки и сосредоточился на бизнес-логике, как только освоил эту возможность. Это как иметь встроенный статический анализатор типов прямо в рантайме.
А еще Pydantic умеет преобразовывать данные между различными форматами. Метод .dict() мы уже видели, но есть еще .json() для сериализации в JSON-строку и .parse_obj() для создания модели из словаря.
| Python | 1
2
3
4
5
6
7
| # Преобразование модели в JSON-строку
product_json = product.json()
# Преобразование из словаря в модель
product_data = {"name": "Молоко", "price": 60.5, "manufactured": "2023-05-01",
"category": {"name": "Молочные продукты"}}
product = ProductDetailed.parse_obj(product_data) |
|
Загрузка и обработка файлов
Работа с файлами - еще одна область, где FastAPI блистает. В прошлом я писал десятки строк кода для обработки загрузки файлов в Flask, а с FastAPI это делается буквально в пару строк:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| from fastapi import FastAPI, File, UploadFile
import shutil
from pathlib import Path
@app.post("/upload/")
async def upload_file(file: UploadFile = File(...)):
upload_dir = Path("uploads")
upload_dir.mkdir(exist_ok=True)
# Сохраняем загруженный файл
file_path = upload_dir / file.filename
with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
return {"filename": file.filename, "size": file_path.stat().st_size} |
|
Здесь я использую класс UploadFile, который предоставляет методы для работы с загруженным файлом, включая асинхронное чтение. Это намного удобнее, чем работать с сырыми байтами.
А если нужно обработать несколько файлов одновременно:
| Python | 1
2
3
4
5
6
7
| @app.post("/upload-multiple/")
async def upload_multiple_files(files: List[UploadFile] = File(...)):
results = []
for file in files:
# Здесь может быть любая обработка файла
results.append({"filename": file.filename, "content_type": file.content_type})
return results |
|
Кстати, FastAPI также поддерживает форматы данных, отличные от JSON. Например, вы можете получать и отправлять файлы как часть ответа:
| Python | 1
2
3
4
5
6
7
8
9
| from fastapi.responses import FileResponse
@app.get("/download/{filename}")
async def download_file(filename: str):
file_path = Path("uploads") / filename
if not file_path.exists():
raise HTTPException(status_code=404, detail="Файл не найден")
return FileResponse(file_path) |
|
Работа с заголовками HTTP и cookies
Часто API-приложениям нужен доступ к HTTP-заголовкам и cookies. FastAPI делает это предельно просто:
| Python | 1
2
3
4
5
6
7
8
9
| from fastapi import FastAPI, Header, Cookie
@app.get("/headers/")
async def read_headers(user_agent: str = Header(None)):
return {"User-Agent": user_agent}
@app.get("/cookies/")
async def read_cookies(session: str = Cookie(None)):
return {"session": session} |
|
А если нужно установить cookie в ответе:
| Python | 1
2
3
4
5
6
7
8
| from fastapi.responses import JSONResponse
@app.post("/login/")
async def login(username: str):
content = {"message": f"Привет, {username}"}
response = JSONResponse(content=content)
response.set_cookie(key="session", value="secret-token")
return response |
|
Я часто использую этот паттерн в микросервисных архитектурах, где нужно передавать токены между сервисами. FastAPI делает это настолько прозрачно, что код остается чистым и понятным.
Продвинутая обработка ошибок
Мы уже видели базовое использование HTTPException, но FastAPI позволяет настроить обработку ошибок глобально, что особенно полезно для больших проектов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
app = FastAPI()
class CustomException(Exception):
def __init__(self, name: str):
self.name = name
@app.exception_handler(CustomException)
async def custom_exception_handler(request: Request, exc: CustomException):
return JSONResponse(
status_code=418,
content={"message": f"Упс! {exc.name} сломался. Мы уже чиним!"},
)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 42:
raise CustomException(name="Продукт")
return {"item_id": item_id} |
|
В этом примере я создал пользовательское исключение CustomException и глобальный обработчик для него. Теперь, когда это исключение возникает где-либо в коде, FastAPI автоматически преобразует его в ответ с кодом 418 (I'm a teapot - да, такой статус-код действительно существует!).
Это чрезвычайно полезно для централизованной обработки ошибок. В одном из моих проектов я создал иерархию исключений для разных типов бизнес-ошибок, и вся обработка была вынесена в отдельный модуль. Код основных эндпоинтов остался чистым, без загромождения блоками try-except.
Dependency Injection и безопасность
Одна из самых недооцененных, но мощных фич FastAPI - это система внедрения зависимостей (dependency injection). Она позволяет:
1. Повторно использовать общую логику между эндпоинтами,
2. Разделять код на маленькие, тестируемые функции,
3. Реализовать аутентификацию и авторизацию
Давайте посмотрим на простой пример: допустим, нам нужно проверять API-ключ для некоторых эндпоинтов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| from fastapi import FastAPI, Depends, HTTPException, Header
app = FastAPI()
async def verify_api_key(x_api_key: str = Header(None)):
if x_api_key != "секретный_ключ":
raise HTTPException(status_code=403, detail="Неверный API-ключ")
return x_api_key
@app.get("/secure-endpoint/", dependencies=[Depends(verify_api_key)])
async def secure_endpoint():
return {"message": "Это защищенный эндпоинт"}
@app.get("/another-secure/")
async def another_secure(api_key: str = Depends(verify_api_key)):
return {"message": f"Ваш API-ключ: {api_key}"} |
|
Я использую функцию verify_api_key как зависимость двумя способами:
1. Как просто зависимость для эндпоинта, без доступа к её результату.
2. Как параметр функции, чтобы получить результат зависимости.
Возможности Depends идут гораздо дальше. Вы можете создавать классы зависимостей с параметрами, комбинировать зависимости и даже использовать их для всего приложения с помощью app.dependency_overrides.
В одном из моих проектов я реализовал многоуровневую систему авторизации с ролями и разрешениями, используя только механизм зависимостей FastAPI. Это избавило меня от необходимости внедрять тяжеловесные фреймворки аутентификации.
Зависимости в FastAPI также могут быть условными. Например, в одном из моих проектов мне нужно было реализовать разные уровни доступа для API: некоторые эндпоинты должны быть доступны анонимно, другие - только зарегестрированным пользователям, а третьи - только админам. Вместо написания отдельной логики для каждого эндпоинта, я создал многоуровневую систему:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| def get_current_user(token: str = Depends(oauth2_scheme)):
# Проверка токена и возврат пользователя
user = decode_token(token)
if user is None:
raise HTTPException(
status_code=401,
detail="Недействительные учетные данные"
)
return user
def get_admin_user(current_user: User = Depends(get_current_user)):
if current_user.role != "admin":
raise HTTPException(
status_code=403,
detail="Недостаточно прав"
)
return current_user
@app.get("/users/me")
def read_users_me(current_user: User = Depends(get_current_user)):
return current_user
@app.get("/admin/stats")
def admin_stats(admin: User = Depends(get_admin_user)):
return {"total_users": get_users_count()} |
|
Красота этого подхода в том, что зависимости могут сами иметь зависимости! get_admin_user зависит от get_current_user, образуя цепочку проверок. Код становится очень чистым и легко тестируемым.
WebSocket поддержка
FastAPI также имеет встроенную поддержку WebSocket для создания приложений реального времени. Я использовал эту возможность для создания системы уведомлений в одном из проектов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| from fastapi import FastAPI, WebSocket, WebSocketDisconnect
app = FastAPI()
# Хранилище для активных соединений
class ConnectionManager:
def __init__(self):
self.active_connections = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
# Эхо индивидуальному клиенту
await websocket.send_text(f"Вы сказали: {data}")
# Рассылка всем клиентам
await manager.broadcast(f"Клиент #{client_id} говорит: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast(f"Клиент #{client_id} покинул чат") |
|
Этот простой код реализует чат-сервер с рассылкой сообщений всем подключеным клиентам. Можно легко расширить его для реализации приватных сообщений, каналов или других паттернов обмена данными в реальном времени.
В моем проекте я добавил к этому шифрование сообщений и систему авторизации на WebSocket, что позволило создать безопасный канал обмена конфиденциальными данными между клиентом и сервером.
Middleware и обработка запросов
Middleware (промежуточное ПО) в FastAPI позволяет выполнять код до и после обработки запроса. Это отлично подходит для добавления логирования, измерения времени выполнения, проверки заголовков и т.д.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| import time
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response |
|
Этот middleware добавляет заголовок с временем обработки запроса. Функция call_next вызывает следующий middleware или сам обработчик запроса, а затем возвращает ответ. Очень полезный паттерн - логирование запросов и ответов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import logging
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
logger = logging.getLogger("api")
@app.middleware("http")
async def log_requests(request: Request, call_next):
logger.info(f"Request: {request.method} {request.url}")
try:
response = await call_next(request)
logger.info(f"Response: {response.status_code}")
return response
except Exception as e:
logger.error(f"Error processing request: {str(e)}")
return JSONResponse(status_code=500, content={"detail": "Internal Server Error"}) |
|
Я часто использую middleware для добавления кастомных заголовков безопасности, как CORS, CSP и других:
| Python | 1
2
3
4
5
6
7
| @app.middleware("http")
async def add_security_headers(request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Content-Security-Policy"] = "default-src 'self'"
return response |
|
Кстати, FastAPI уже имеет встроенную поддержку CORS, которую можно настроить так:
| Python | 1
2
3
4
5
6
7
8
9
| from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost", "https://example.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
) |
|
Асинхронность в FastAPI
Одна из главных особенностей FastAPI - поддержка асинхронного программирования с помощью async/await. В отличие от Flask и Django, где асинхронность - это скорее дополнение, в FastAPI это встроенная возможность.
Когда стоит использовать асинхронные обработчики?- При операциях ввода-вывода: запросы к базам данных, API или файловой системе,
- При обработке множества параллельных запросов,
- Для долгих операций, которые могут блокировать основной поток.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import asyncio
from fastapi import FastAPI
app = FastAPI()
@app.get("/sync")
def sync_route():
# Блокирующая операция - блокирует весь сервер
import time
time.sleep(1)
return {"message": "Синхронный путь"}
@app.get("/async")
async def async_route():
# Неблокирующая операция - другие запросы обрабатываются параллельно
await asyncio.sleep(1)
return {"message": "Асинхронный путь"} |
|
В этом примере /sync блокирует весь сервер на 1 секунду, в то время как /async позволяет обрабатывать другие запросы, пока эта функция "спит".
Асинхронность особенно полезна при работе с базами данных. Например, с асинхронным драйвером для MongoDB:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| from fastapi import FastAPI
from motor.motor_asyncio import AsyncIOMotorClient
app = FastAPI()
@app.on_event("startup")
async def startup_db_client():
app.mongodb_client = AsyncIOMotorClient("mongodb://localhost:27017")
app.mongodb = app.mongodb_client.db
@app.on_event("shutdown")
async def shutdown_db_client():
app.mongodb_client.close()
@app.get("/users/{user_id}")
async def get_user(user_id: str):
user = await app.mongodb.users.find_one({"_id": user_id})
if user:
return user
return {"error": "User not found"} |
|
В моем опыте, переход от синхронных к асинхронным операциям с базой данных увеличил пропускную способность API примерно в 10 раз при тех же затратах ресурсов. Но есть и подводные камни: асинхронный код сложнее отлаживать, а если вы используете блокирующие библиотеки внутри асинхронных функций, производительность может даже упасть.
Мой совет: используйте асинхронность для операций ввода-вывода, но не переписывайте все подряд - иногда синхронный код проще и понятнее.
Есть и другие крутые возможности FastAPI для асинхронного программирования, например, фоновые задачи:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
| from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
def write_log(message: str):
with open("log.txt", "a") as f:
f.write(message + "\n")
@app.post("/send-notification/{email}")
async def send_notification(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(write_log, f"Отправка уведомления на {email}")
return {"message": "Уведомление будет отправлено в фоновом режиме"} |
|
Метод add_task добавляет функцию в очередь задач, которые будут выполнены после отправки ответа клиенту. Это отличный способ обрабатывать долгие операции, не заставляя пользователя ждать.
Типизация запросов и ответов
Я всегда был фанатом строгой типизации, и FastAPI - это настоящий подарок для таких, как я. Аннотации типов здесь не просто документация, а рабочий механизм, который помогает избежать множества ошибок еще на этапе разработки. Допустим, вы хотите создать эндпоинт, который принимает список продуктов и возвращает общую стоимость:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| from typing import List
from pydantic import BaseModel
from fastapi import FastAPI
app = FastAPI()
class Product(BaseModel):
name: str
price: float
quantity: int = 1
@app.post("/calculate-total/")
async def calculate_total(products: List[Product]):
total = sum(product.price * product.quantity for product in products)
return {"total": total} |
|
Здесь типизация работает на нескольких уровнях:
1. FastAPI проверяет, что тело запроса - это список объектов.
2. Pydantic проверяет, что каждый объект соответствует модели Product.
3. IDE показывает подсказки с типами при работе с полями продукта.
Но это только начало. Вы можете определять типы для всего: параметров пути, запроса, заголовков и даже для возвращаемых данных.
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
| from typing import Dict, Any, Union, Optional
from fastapi import FastAPI, Query, Path, Body
app = FastAPI()
@app.get("/items/{item_id}", response_model=Dict[str, Any])
async def read_item(
item_id: int = Path(..., title="ID товара"),
q: Optional[str] = Query(None, max_length=50),
skip: int = Query(0, ge=0),
limit: Union[int, None] = Query(10, le=100)
):
# ...
return {"item_id": item_id, "q": q, "skip": skip, "limit": limit} |
|
Параметр response_model особенно полезен - он не только валидирует данные перед отправкой, но и фильтрует лишние поля. Это критично для безопасности, когда вы не хотите случайно раскрыть конфиденциальную информацию. Я часто использую это для разделения внутренних и публичных представлений данных:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| class UserInDB(BaseModel):
id: int
username: str
password_hash: str
email: str
is_admin: bool = False
class UserPublic(BaseModel):
id: int
username: str
email: str
@app.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int):
# Предположим, это реальный объект с хешем пароля и другими секретами
user = UserInDB(
id=user_id,
username="johndoe",
password_hash="supersecret",
email="john@example.com",
is_admin=True
)
return user # FastAPI автоматически отфильтрует поля password_hash и is_admin |
|
В одном из моих проектов это спасло от серьезной уязвимости - джуниор случайно вернул полный объект пользователя, включая хеш пароля, но благодаря response_model клиент получил только безопасные поля.
Декораторы для маршрутизации и организации кода
FastAPI предлагает набор декораторов для определения маршрутов, которые соответствуют HTTP-методам:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| @app.get("/items/")
async def read_items():
return [{"name": "Item 1"}, {"name": "Item 2"}]
@app.post("/items/")
async def create_item(item: dict):
return item
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: dict):
return {"item_id": item_id, **item}
@app.delete("/items/{item_id}")
async def delete_item(item_id: int):
return {"deleted": item_id}
@app.patch("/items/{item_id}")
async def partial_update(item_id: int, item: dict):
return {"item_id": item_id, **item} |
|
Но помимо основных методов, есть еще @app.head(), @app.options() и даже @app.trace(). Я редко использую их напрямую, но они бывают полезны для совместимости с некоторыми клиентами.
Для организации кода в больших проектах, FastAPI предлагает концепцию "APIRouter". Это как мини-приложения, которые можно подключать к основному. Я обычно организую код по доменным областям:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| from fastapi import APIRouter, FastAPI
app = FastAPI()
# Роутер для пользователей
user_router = APIRouter(prefix="/users", tags=["users"])
@user_router.get("/")
async def list_users():
return [{"username": "rick"}, {"username": "morty"}]
@user_router.get("/{username}")
async def get_user(username: str):
return {"username": username}
# Роутер для товаров
product_router = APIRouter(prefix="/products", tags=["products"])
@product_router.get("/")
async def list_products():
return [{"name": "Hammer"}, {"name": "Wrench"}]
# Подключаем роутеры к основному приложению
app.include_router(user_router)
app.include_router(product_router) |
|
Параметр tags особенно полезен - он группирует эндпоинты в Swagger UI, что делает документацию более структурированной. А prefix избавляет от необходимости повторять базовый путь для каждого эндпоинта.
В одном из наших проектов мы пошли дальше и создали фабрику роутеров для стандартных CRUD-операций:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| def create_crud_router(model, db_dependency):
router = APIRouter()
@router.get("/")
async def list_items(db = Depends(db_dependency)):
return await db.find_all(model)
@router.get("/{item_id}")
async def get_item(item_id: str, db = Depends(db_dependency)):
return await db.find_one(model, item_id)
# ... другие CRUD-методы
return router
# Использование
user_router = create_crud_router(User, get_db)
app.include_router(user_router, prefix="/users", tags=["users"]) |
|
Это сэкономило нам кучу повторяющегося кода и сделало API более консистентным.
Кеширование и управление состоянием
FastAPI не имеет встроенного механизма кеширования, но его легко добавить с помощью зависимостей и декораторов. Я часто использую такой паттерн:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| from fastapi import FastAPI, Depends
from functools import lru_cache
app = FastAPI()
class Settings:
def __init__(self):
# В реальном проекте здесь могла бы быть загрузка из .env файла
self.app_name = "Awesome API"
self.admin_email = "admin@example.com"
self.items_per_page = 20
@lru_cache()
def get_settings():
return Settings()
@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email
} |
|
Декоратор @lru_cache() из стандартной библиотеки Python кеширует результат функции, так что Settings создается только один раз, а не при каждом запросе.
Для более сложных сценариев кеширования я использую Redis:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| import aioredis
from fastapi import FastAPI, Depends
import json
app = FastAPI()
async def get_redis():
redis = await aioredis.create_redis_pool("redis://localhost")
try:
yield redis
finally:
redis.close()
await redis.wait_closed()
@app.get("/products/{product_id}")
async def get_product(product_id: int, redis = Depends(get_redis)):
# Пробуем получить из кеша
cached = await redis.get(f"product:{product_id}")
if cached:
return json.loads(cached)
# Если нет в кеше, получаем из БД
product = await db.get_product(product_id)
# Сохраняем в кеш на 1 час
await redis.set(
f"product:{product_id}",
json.dumps(product),
expire=3600
)
return product |
|
Такой подход значительно снижает нагрузку на базу данных и ускоряет ответы API. В одном из проектов это снизило среднее время ответа с 200мс до 15мс - впечатляющее улучшение!
Интеграция с базами данных через SQLAlchemy
Большинство реальных проектов так или иначе используют базы данных. FastAPI прекрасно работает с SQLAlchemy — самым популярным ORM для Python. Вот базовая настройка асинхронного SQLAlchemy:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker, declarative_base
from sqlalchemy import Column, Integer, String, Float
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname"
Base = declarative_base()
engine = create_async_engine(DATABASE_URL)
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Product(Base):
__tablename__ = "products"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
description = Column(String)
price = Column(Float)
app = FastAPI()
async def get_db():
db = AsyncSessionLocal()
try:
yield db
finally:
await db.close()
@app.get("/products/{product_id}")
async def get_product(product_id: int, db: AsyncSession = Depends(get_db)):
from sqlalchemy import select
query = select(Product).where(Product.id == product_id)
result = await db.execute(query)
product = result.scalars().first()
if not product:
raise HTTPException(status_code=404, detail="Продукт не найден")
return product |
|
Обратите внимание на использование asyncpg — это асинхронный драйвер для PostgreSQL, который делает запросы неблокирующими. В боевом приложении я обычно выношу схемы моделей и функции доступа к БД в отдельные модули для лучшей организации кода.
Частая ошибка новичков — создание сессии базы данных для каждого запроса вручную. Зависимость get_db решает эту проблему элегантно и обеспечивает автоматическое закрытие соединения даже при возникновении исключения.
Безопасность и аутентификация
Безопасность критична для любого API. FastAPI имеет встроенную поддержку OAuth2 с JWT-токенами:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
| from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from pydantic import BaseModel
# Настройки безопасности
SECRET_KEY = "ваш_секретный_ключ"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
username: str
email: str = None
full_name: str = None
disabled: bool = False
class Token(BaseModel):
access_token: str
token_type: str
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_user(db, username: str):
# В реальности - запрос к БД
if username == "testuser":
return {"username": "testuser", "hashed_password": pwd_context.hash("password")}
def authenticate_user(db, username: str, password: str):
user = get_user(db, username)
if not user:
return False
if not verify_password(password, user["hashed_password"]):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
app = FastAPI()
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = authenticate_user(None, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Неверное имя пользователя или пароль",
headers={"WWW-Authenticate": "Bearer"},
)
access_token = create_access_token(
data={"sub": user["username"]},
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
)
return {"access_token": access_token, "token_type": "bearer"}
async def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Недействительные учетные данные",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user(None, username=username)
if user is None:
raise credentials_exception
return user
@app.get("/users/me", response_model=User)
async def read_users_me(current_user = Depends(get_current_user)):
return current_user |
|
В реальных проектах я также часто реализую:- Систему ролей и разрешений,
- Ограничение частоты запросов (rate limiting),
- Защиту от CSRF и XSS атак,
- Двухфакторную аутентификацию,
Не секрет, что большинство уязвимостей в API происходят из-за некорректной проверки аутентификации и авторизации. FastAPI упрощает правильную реализацию этих механизмов, но вам все равно нужно хорошо понимать основы безопасности.
Структура проекта для реальных задач
В боевых проектах я всегда следую подходу с разделением кода на модули. Вот структура, которую я обычно использую:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| my_app/
├── alembic/ # Миграции базы данных
├── app/
│ ├── __init__.py
│ ├── main.py # Точка входа FastAPI
│ ├── config.py # Конфигурация приложения
│ ├── dependencies.py # Общие зависимости
│ ├── api/
│ │ ├── __init__.py
│ │ ├── v1/ # API версии 1
│ │ │ ├── __init__.py
│ │ │ ├── endpoints/ # Маршруты сгруппированы по ресурсам
│ │ │ │ ├── __init__.py
│ │ │ │ ├── users.py
│ │ │ │ ├── products.py
│ │ │ │ └── ...
│ │ │ └── router.py # Сборка всех маршрутов v1
│ │ └── v2/ # API версии 2
│ ├── core/ # Ядро приложения
│ │ ├── __init__.py
│ │ ├── security.py # JWT, аутентификация
│ │ └── errors.py # Обработчики ошибок
│ ├── crud/ # Функции CRUD
│ │ ├── __init__.py
│ │ ├── base.py # Базовый класс CRUD
│ │ ├── users.py
│ │ └── ...
│ ├── db/ # Настройки БД
│ │ ├── __init__.py
│ │ ├── base.py
│ │ └── session.py
│ ├── models/ # SQLAlchemy модели
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── ...
│ └── schemas/ # Pydantic модели
│ ├── __init__.py
│ ├── user.py
│ └── ...
├── tests/ # Тесты
└── .env # Переменные окружения |
|
Такая структура обеспечивает хорошую организацию кода и облегчает его поддержку и масштабирование. Конечно, не нужно слепо следовать этому шаблону — адаптируйте его под свои нужды. Один из ключевых принципов, который я применяю — это строгое разделение между моделями БД (SQLAlchemy) и схемами API (Pydantic). Это позволяет независимо развивать структуру базы данных и API-контракты.
Фоновые задачи и очереди
Для долгих операций, таких как отправка email или обработка файлов, я использую фоновые задачи. FastAPI предлагает простой механизм с BackgroundTasks, но для сложных сценариев лучше использовать Celery или арину:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| from fastapi import FastAPI, BackgroundTasks
from fastapi.responses import JSONResponse
import aioredis
import aiohttp
from arq import create_pool
from arq.connections import RedisSettings
app = FastAPI()
async def process_video(ctx, video_id: str):
# Долгая обработка видео
return {"processed": True, "video_id": video_id}
@app.on_event("startup")
async def startup():
app.redis = await create_pool(RedisSettings(host="localhost"))
@app.on_event("shutdown")
async def shutdown():
await app.redis.close()
@app.post("/videos/{video_id}/process")
async def start_video_processing(video_id: str):
# Добавляем задачу в очередь
job = await app.redis.enqueue_job("process_video", video_id)
return {"job_id": job.job_id, "status": "processing"}
@app.get("/jobs/{job_id}")
async def get_job_status(job_id: str):
job = await app.redis.get_job_result(job_id)
if job is None:
return {"status": "processing"}
return {"status": "completed", "result": job} |
|
В этом примере я использую арину — легковесную асинхронную очередь задач, которая хорошо работает с FastAPI. Для более сложных сценариев с приоритетами, ретраями и мониторингом я бы выбрал Celery с RabbitMQ.
Важный момент: никогда не запускайте тяжелые вычисления прямо в обработчике FastAPI — это блокирует весь сервер. Всегда выносите их в фоновые задачи или микросервисы.
Кеширование и оптимизация производительности
В высоконагруженных системах кеширование становится не роскошью, а необходимостью. Я много раз спасал проекты от перегрузки, добавляя правильную стратегию кеширования. С FastAPI это делается особенно элегантно благодаря системе зависимостей. Помимо уже упомянутого Redis, я часто использую кеширование на уровне запросов с помощью HTTP-заголовков:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| from fastapi import FastAPI, Request, Response, Depends
from fastapi.responses import JSONResponse
import time
import hashlib
app = FastAPI()
def generate_etag(data: dict) -> str:
# Создаем хеш содержимого для ETag
content = str(sorted(data.items())).encode()
return hashlib.md5(content).hexdigest()
@app.middleware("http")
async def add_cache_headers(request: Request, call_next):
response = await call_next(request)
# Добавляем заголовки кеширования только для GET-запросов
if request.method == "GET":
response.headers["Cache-Control"] = "max-age=60" # Кешируем на 1 минуту
# Если есть тело ответа, добавляем ETag
if hasattr(response, "body"):
try:
body = response.body.decode()
import json
data = json.loads(body)
etag = generate_etag(data)
response.headers["ETag"] = etag
except:
pass
return response |
|
Этот мидлвар добавляет заголовки кеширования для GET-запросов и генерирует ETag на основе содержимого ответа. Клиенты могут использовать это для условных запросов, что значительно снижает нагрузку на сервер. Но кеширование - это только часть истории. Для действительно высокопроизводительных API я применяю:
1. Оптимизация запросов к БД: Не только индексы, но и правильное использование select_related и prefetch_related в ORM, чтобы избежать N+1 запросов.
2. Пагинация и курсоры: Для больших наборов данных вместо обычной пагинации с limit/offset использую курсоры, особенно на постоянно обновляющихся данных:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| @app.get("/users/")
async def list_users(cursor: str = None, limit: int = 20):
if cursor:
# Декодируем курсор (обычно это закодированный timestamp последнего элемента)
import base64
last_timestamp = float(base64.b64decode(cursor).decode())
users = await db.fetch_users(
where="created_at < :timestamp",
params={"timestamp": last_timestamp},
limit=limit + 1, # +1 чтобы узнать, есть ли еще элементы
order_by="created_at DESC"
)
else:
users = await db.fetch_users(limit=limit + 1, order_by="created_at DESC")
has_more = len(users) > limit
result = users[:limit] # Отрезаем лишний элемент
# Создаем новый курсор, если есть еще элементы
next_cursor = None
if has_more and result:
last_user = result[-1]
next_cursor = base64.b64encode(str(last_user["created_at"]).encode()).decode()
return {
"users": result,
"has_more": has_more,
"next_cursor": next_cursor
} |
|
3. Сжатие ответов: FastAPI автоматически поддерживает сжатие ответов Gzip и Brotli, но нужно убедиться, что ваш сервер настроен правильно:
| Python | 1
2
3
| from fastapi.middleware.gzip import GZipMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000) # Сжимать ответы больше 1KB |
|
Один из самых недооцененных аспектов оптимизации - профилирование. Я регулярно использую инструменты вроде py-spy для анализа узких мест:
| Bash | 1
2
| # Профилирование запущеного приложения
py-spy record -o profile.svg --pid $(pgrep -f "uvicorn main:app") |
|
Это даёт наглядную диаграмму вызовов, где сразу видно, какие функции занимают больше всего времени.
Контейнеризация FastAPI приложений
Docker и FastAPI - просто созданы друг для друга. Я контейнеризирую все свои FastAPI-приложения для упрощения развертывания и масштабирования. Вот базовый Dockerfile, который я использую:
| Windows Batch file | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # Базовый образ Python
FROM python:3.10-slim
# Рабочая директория внутри контейнера
WORKDIR /app
# Устанавливаем зависимости
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копируем код приложения
COPY ./app /app/app
# Устанавливаем переменные окружения
ENV PORT=8000
# Запускаем приложение
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "${PORT}"] |
|
Но в продакшн я обычно использую многоэтапную сборку, чтобы уменьшить размер образа:
| Windows Batch file | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| # Сборочный этап
FROM python:3.10-slim AS builder
WORKDIR /app
# Устанавливаем только необходимые пакеты для сборки
RUN pip install --no-cache-dir poetry
# Копируем только файлы зависимостей
COPY pyproject.toml poetry.lock* ./
# Экспортируем зависимости в requirements.txt
RUN poetry export -f requirements.txt > requirements.txt
# Финальный этап
FROM python:3.10-slim
WORKDIR /app
# Копируем requirements.txt из предыдущего этапа
COPY --from=builder /app/requirements.txt .
# Устанавливаем зависимости
RUN pip install --no-cache-dir -r requirements.txt
# Копируем код приложения
COPY ./app /app/app
# Пользователь без привилегий для безопасности
RUN useradd -m appuser
USER appuser
# Конфигурация для продакшена
ENV PORT=8000
ENV WORKERS=4
# Запускаем с несколькими рабочими процессами
CMD uvicorn app.main:app --host 0.0.0.0 --port $PORT --workers $WORKERS |
|
Важный момент - количество воркеров. Я обычно использую формулу (2 * CPU_cores) + 1. На 4-ядерном сервере это будет 9 воркеров. Но не забывайте, что каждый воркер потребляет память, так что нужен баланс. Для еще большей производительности я иногда использую Gunicorn с Uvicorn-воркерами:
| Bash | 1
| gunicorn app.main:app -w 4 -k uvicorn.workers.UvicornWorker |
|
Это даёт преимущества обоих миров: стабильность Gunicorn и скорость Uvicorn.
Что касается docker-compose, вот базовая конфигурация для разработки:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| version: '3'
services:
api:
build: .
ports:
- "8000:8000"
volumes:
- ./app:/app/app
environment:
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@db/app
- DEBUG=true
depends_on:
- db
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
db:
image: postgres:14
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=app
volumes:
postgres_data: |
|
Обратите внимание на флаг --reload - он позволяет изменять код и видить результаты без перезапуска контейнера, что очень удобно в разработке.
Развертывание и мониторинг
После того как приложение контейнеризировано, его можно развернуть где угодно: Kubernetes, Heroku, AWS ECS и т.д. Я обычно использую GitHub Actions для CI/CD:
| YAML | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| name: Deploy
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: myregistry.io/myapp:latest
- name: Deploy to Kubernetes
uses: steebchen/kubectl@v2
with:
config: ${{ secrets.KUBE_CONFIG }}
command: apply -f k8s/deployment.yaml |
|
Для мониторинга FastAPI приложений я использую комбинацию Prometheus и Grafana. FastAPI легко интегрируется с Prometheus через starlette-exporter:
| Python | 1
2
3
4
5
6
7
8
9
10
| from fastapi import FastAPI
from starlette_exporter import PrometheusMiddleware, handle_metrics
app = FastAPI()
# Добавляем middleware для сбора метрик
app.add_middleware(PrometheusMiddleware)
# Эндпоинт для Prometheus
app.add_route("/metrics", handle_metrics) |
|
Теперь у вас есть эндпоинт /metrics, который возвращает метрики в формате Prometheus: количество запросов, время ответа, статус коды и т.д.
Для логирования я настоятельно рекомендую использовать структурированные логи в формате JSON - это делает их анализ намного проще:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| import logging
import json
from datetime import datetime
class JSONFormatter(logging.Formatter):
def format(self, record):
log_record = {
"timestamp": datetime.utcnow().isoformat(),
"level": record.levelname,
"message": record.getMessage(),
"module": record.module,
"function": record.funcName,
}
if hasattr(record, "request_id"):
log_record["request_id"] = record.request_id
if record.exc_info:
log_record["exception"] = self.formatException(record.exc_info)
return json.dumps(log_record)
# Настройка логгера
logger = logging.getLogger("api")
handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Middleware для добавления request_id в логи
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
# Добавляем request_id в контекст логгера
logger_with_context = logging.LoggerAdapter(
logger, {"request_id": request_id}
)
# Используем этот логгер в обработчике
request.state.logger = logger_with_context
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response |
|
Такой подход позволяет легко трассировать запросы через разные сервисы и микросервисы.
Полный листинг готового приложения
И в конце, как я обещал - полный листинг рабочего FastAPI-приложения с основными возможностями, которые мы обсуждали. Вы можете использовать его как стартовую точку для своих проектов:
| Python | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
| from fastapi import FastAPI, Depends, HTTPException, Query, Path
from fastapi.middleware.cors import CORSMiddleware
from typing import List, Optional
from pydantic import BaseModel, Field
import uvicorn
app = FastAPI(title="Простой API каталога продуктов")
# Middleware для CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Модели данных
class ProductBase(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
price: float = Field(..., gt=0)
description: Optional[str] = None
class ProductCreate(ProductBase):
pass
class ProductResponse(ProductBase):
id: int
# Имитация базы данных
products_db = []
# Маршруты API
@app.post("/products/", response_model=ProductResponse)
def create_product(product: ProductCreate):
new_product = product.dict()
new_product["id"] = len(products_db)
products_db.append(new_product)
return new_product
@app.get("/products/{product_id}", response_model=ProductResponse)
def read_product(product_id: int = Path(..., ge=0)):
if product_id >= len(products_db):
raise HTTPException(status_code=404, detail="Продукт не найден")
return products_db[product_id]
@app.get("/products/", response_model=List[ProductResponse])
def list_products(
skip: int = Query(0, ge=0),
limit: int = Query(10, ge=1, le=100),
min_price: Optional[float] = Query(None, ge=0)
):
result = products_db
if min_price is not None:
result = [p for p in result if p["price"] >= min_price]
return result[skip : skip + limit]
if __name__ == "__main__":
uvicorn.run("main:app", reload=True) |
|
Просто сохраните этот код в файл main.py, установите зависимости (pip install fastapi uvicorn) и запустите с помощью python main.py. Готово! У вас есть полноценный API с валидацией данных, обработкой ошибок и автоматической документацией. Перейдите на http://127.0.0.1:8000/docs, чтобы увидеть и протестировать свои эндпоинты.
FastAPI и pytest Использовал инструкцию https://www.fastapitutorial.com/blog/unit-testing-in-fastapi/
но что-то не... Django vs. Flask vs. FastAPI Какой фреймворк выбрать начинающему? Какой проще, какой сложнее? Для какого больше дополнительных... JSON поле в peewee + pydantic (FastAPI) Доброго времени. Проблема следующая.
Есть приложение на FastAPI, в котором описаны модели таблиц... FastAPI и matplotlib Напишите API сервис используя фреймворк FastAPI
Он должен уметь принимать данные (На ваше... Ошибка NameError при применении FastAPI Всем добрый день :)
Подскажите, пожалуйста:
есть функция, в которую передаю аргумент через... FastAPI выгрузить html код возникла небольшая проблема я решил при помощи фраемворка выгрузить html код(так по себе он... ImportError FastAPI Всем, привет, прохожу сейчас курс по FastAPI. Остановился на аутентификации, из-за того, что при... FastAPI и приложение для протоколирования собрания Добрый день!
Я учусь python не так давно и мне нужно написать бэк для веб-приложения для... Аутентификация в FastAPI Добрый день.
Написал в FastAPI вот такой эндпоинт:
from fastapi import FastAPI, Depends,... FastAPI SQLAlchemy запрос вывести в JSON или CSV? Выполнил сложный sql запрос через sqlalchemy, результат в консоль печатается. Надо выводить в... Чем async Flask хуже FastAPI? День добрый.
На рынке видно тренды Flask, FastAPI. Порой (возможно), компании отдают... В январе Саше подарили пару новорожденных кроликов. Через два месяца они дали первый приплод – новую пару кроликов суть задания: В январе Саше подарили пару новорожденных кроликов. Через два месяца они дали первый...
|