FastAPI: эндпоинты, параметры, body, OpenAPI
FastAPI: эндпоинты, параметры, body, OpenAPI
FastAPI - современный ASGI-фреймворк для построения REST API на Python. Async-first, type hints для валидации, автоматическая документация OpenAPI/Swagger. В 2026 году де-факто стандарт для нового backend кода. Для сравнения: в Go HTTP-сервер собирают руками из net/http - смотри урок про HTTP в Go. В этом уроке - базовое создание эндпоинтов с разными типами параметров, response models, статус-коды и Swagger UI.
Установка и первый запуск
pip install "fastapi[all]"
Минимальный пример:
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {"message": "Hello, FastAPI"}
Запуск:
uvicorn main:app --reload
# --reload автоперезагружает при изменениях - dev mode
Откроется на http://localhost:8000. Документация:
http://localhost:8000/docs- Swagger UIhttp://localhost:8000/redoc- ReDoc альтернативный UIhttp://localhost:8000/openapi.json- OpenAPI спецификация
Документация генерируется автоматически из кода и type hints.
HTTP методы
@app.get("/users") # GET
@app.post("/users") # POST
@app.put("/users/{id}") # PUT
@app.patch("/users/{id}") # PATCH
@app.delete("/users/{id}") # DELETE
Соответствуют REST конвенциям: GET для чтения, POST для создания, PUT/PATCH для обновления, DELETE для удаления.
Path параметры
Параметры в URL через {name}:
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
Type hints автоматически валидируют. GET /users/abc даст 422 Unprocessable Entity потому что abc не int. GET /users/42 сработает с user_id=42.
С Enum для ограниченных значений:
from enum import Enum
class Role(str, Enum):
admin = "admin"
user = "user"
guest = "guest"
@app.get("/users/role/{role}")
def by_role(role: Role):
return {"role": role}
# GET /users/role/admin - OK
# GET /users/role/banana - 422
Query параметры
Параметры функции, не упомянутые в path - это query параметры:
@app.get("/users")
def list_users(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
# GET /users?skip=20&limit=5
Дефолтные значения делают параметры опциональными. Type hints валидируют типы.
С Query для расширенной валидации:
from fastapi import Query
@app.get("/search")
def search(
q: str = Query(..., min_length=3, max_length=50, regex="^[a-zA-Z]+quot;),
limit: int = Query(10, ge=1, le=100),
):
return {"query": q, "limit": limit}
Query(...) означает обязательный параметр. min_length, ge, le - валидаторы.
Body параметры через Pydantic
Для POST/PUT body парсится через Pydantic-модели (подробно в следующем уроке):
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
email: str
@app.post("/users")
def create_user(user: User):
return {"created": user.name, "user": user}
Тело запроса:
{"name": "Alice", "age": 30, "email": "alice@example.com"}
FastAPI валидирует JSON по модели и передаёт типизированный объект. Если данные невалидны - 422 с подробным описанием.
Statuscodes
from fastapi import status
@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: User):
return {"created": user.name}
HTTP_201_CREATED = 201. По умолчанию POST/PUT возвращают 200 OK, но 201 Created семантически правильнее для создания.
Динамический статус через Response:
from fastapi import Response, status
@app.get("/users/{id}")
def get_user(id: int, response: Response):
user = find_user(id)
if user is None:
response.status_code = status.HTTP_404_NOT_FOUND
return {"error": "not found"}
return user
Или лучше через HTTPException:
from fastapi import HTTPException
@app.get("/users/{id}")
def get_user(id: int):
user = find_user(id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
HTTPException конвертируется в HTTP response автоматически. Стандартный способ обработки ошибок.
Response model
Для документации и фильтрации output:
class UserOut(BaseModel):
id: int
name: str
email: str
# password не включаем намеренно
@app.get("/users/{id}", response_model=UserOut)
def get_user(id: int):
user = db.get_user(id)
return user # automatically filtered to UserOut fields
Если функция возвращает больше полей чем UserOut - они отфильтруются. Это и валидация выхода, и документация для OpenAPI.
response_model_exclude_unset
@app.get("/users/{id}", response_model=User, response_model_exclude_unset=True)
def get_user(id: int):
return user
exclude_unset=True - не включать неустановленные поля. Полезно для PATCH-like endpoints где возвращаются только изменённые поля.
Headers и cookies
from fastapi import Header, Cookie
@app.get("/items")
def get_items(
user_agent: str | None = Header(None),
session_id: str | None = Cookie(None),
):
return {"user_agent": user_agent, "session": session_id}
FastAPI автоматически читает headers и cookies, конвертирует имена (User-Agent → user_agent).
File uploads
from fastapi import File, UploadFile
@app.post("/upload")
async def upload_file(file: UploadFile):
contents = await file.read()
return {
"filename": file.filename,
"content_type": file.content_type,
"size": len(contents),
}
UploadFile поддерживает большие файлы (streaming). Для маленьких можно использовать File().
Async или sync handlers
# Sync - подходит для CPU-bound
@app.get("/compute")
def compute():
return {"result": heavy_calculation()}
# Async - для I/O-bound
@app.get("/users/{id}")
async def get_user(id: int):
user = await db.fetch_user(id)
return user
FastAPI поддерживает оба. Sync handlers запускаются в thread pool автоматически. Async handlers в event loop.
Правило: если есть await - используй async def. Если только sync операции и быстрые - можно def.
Dependencies - dependency injection
Депенденси - функция, результат которой передаётся в handler:
from fastapi import Depends
def common_params(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
@app.get("/items")
def list_items(params: dict = Depends(common_params)):
return params
@app.get("/users")
def list_users(params: dict = Depends(common_params)):
return params
common_params будет вызываться для каждого endpoint, который её использует. Удобно для:
- Pagination параметров
- Auth (получить current_user)
- DB sessions
- Логирования
Async dependencies
async def get_db():
db = AsyncDatabase()
await db.connect()
try:
yield db
finally:
await db.close()
@app.get("/users/{id}")
async def get_user(id: int, db = Depends(get_db)):
return await db.fetch_user(id)
С yield - setup/teardown как в pytest fixtures. Идеально для DB sessions, transactions.
Tags - группировка в Swagger
@app.get("/users", tags=["users"])
def list_users():
...
@app.post("/users", tags=["users"])
def create_user(user: User):
...
@app.get("/orders", tags=["orders"])
def list_orders():
...
В Swagger UI endpoints группируются по тегам. Удобно для больших API.
Метаданные
app = FastAPI(
title="My API",
description="REST API for task management",
version="1.0.0",
contact={"name": "Alice", "email": "alice@example.com"},
license_info={"name": "MIT"},
)
Это попадает в OpenAPI спецификацию и отображается в Swagger.
Routers - модульность
Для больших проектов используй APIRouter:
# routers/users.py
from fastapi import APIRouter
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/")
def list_users():
...
@router.get("/{id}")
def get_user(id: int):
...
# main.py
from routers import users
app = FastAPI()
app.include_router(users.router)
Структура проекта:
CORS - cross-origin для browsers
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Нужно если frontend на другом домене. По умолчанию browser блокирует cross-origin requests.
Тестирование с TestClient
from fastapi.testclient import TestClient
client = TestClient(app)
def test_root():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"message": "Hello, FastAPI"}
def test_create_user():
response = client.post("/users", json={"name": "Alice", "age": 30, "email": "a@b.c"})
assert response.status_code == 200
assert response.json()["created"] == "Alice"
TestClient синхронный, использует httpx. Можно тестировать без запуска сервера. Для async тестов есть AsyncClient из httpx.
Распространённые ошибки
1. Blocking sync в async handler
@app.get("/data")
async def bad():
time.sleep(5) # БЛОКИРУЕТ event loop
requests.get("...") # БЛОКИРУЕТ (синхронный)
Используй async-альтернативы или sync handler (FastAPI запустит в thread pool).
2. Передача больших структур без response_model
@app.get("/users")
def list_users():
return db.get_all() # Что попало в response?
Без response_model нет фильтрации - может утечь password_hash или другие приватные поля. Всегда используй response_model.
3. Не использовать HTTPException
# Плохо - hand-crafted response
def get_user(id: int):
user = find(id)
if user is None:
return {"error": "not found"}, 404 # неправильный return
# Хорошо
def get_user(id: int):
user = find(id)
if user is None:
raise HTTPException(404, "User not found")
return user
4. Игнорировать Pydantic validation
@app.post("/items")
def create(name: str, price: float): # query params, не body
...
Для body нужна Pydantic model. Простые типы без модели интерпретируются как query параметры.
5. Не использовать Depends
@app.get("/users")
def list_users(skip: int = 0, limit: int = 10):
...
@app.get("/orders")
def list_orders(skip: int = 0, limit: int = 10): # дублирование
...
С Depends на pagination function убираешь дублирование.
Полный пример - REST API для todo
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
from typing import Optional
from uuid import uuid4, UUID
app = FastAPI(title="Todo API", version="1.0.0")
class TodoCreate(BaseModel):
title: str
done: bool = False
class TodoOut(BaseModel):
id: UUID
title: str
done: bool
# Imitation БД
todos: dict[UUID, TodoOut] = {}
@app.get("/todos", response_model=list[TodoOut])
def list_todos(skip: int = 0, limit: int = 10):
return list(todos.values())[skip:skip + limit]
@app.post("/todos", response_model=TodoOut, status_code=status.HTTP_201_CREATED)
def create_todo(todo: TodoCreate):
new_id = uuid4()
todo_out = TodoOut(id=new_id, **todo.model_dump())
todos[new_id] = todo_out
return todo_out
@app.get("/todos/{id}", response_model=TodoOut)
def get_todo(id: UUID):
todo = todos.get(id)
if todo is None:
raise HTTPException(404, "Todo not found")
return todo
@app.patch("/todos/{id}", response_model=TodoOut)
def update_todo(id: UUID, todo: TodoCreate):
if id not in todos:
raise HTTPException(404, "Todo not found")
todo_out = TodoOut(id=id, **todo.model_dump())
todos[id] = todo_out
return todo_out
@app.delete("/todos/{id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_todo(id: UUID):
if id not in todos:
raise HTTPException(404, "Todo not found")
del todos[id]
return None
Запусти, открой /docs - увидишь полную интерактивную документацию из этих 30 строк кода.
Сравнение с Go и PHP
В Go обычно используют net/http + router (chi, gin):
r := chi.NewRouter()
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
json.NewEncoder(w).Encode(map[string]string{"id": id})
})
http.ListenAndServe(":8080", r)
В PHP с Symfony/Laravel:
$app->get('/users/{id}', function($id) {
return ['id' => $id];
});
FastAPI ближе к декларативному стилю Laravel/Spring, но с Python type hints для validation. Менее multosложен чем Symfony, более structured чем Flask. Хорошее middle ground для backend разработки.
Мини-задание
- Минимальный API:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Greeting(BaseModel):
name: str
formal: bool = False
@app.get("/")
def root():
return {"status": "ok"}
@app.post("/greet")
def greet(g: Greeting):
prefix = "Уважаемый" if g.formal else "Привет"
return {"message": f"{prefix}, {g.name}!"}
# uvicorn main:app --reload
# Открыть http://localhost:8000/docs
- С обработкой ошибок:
from fastapi import FastAPI, HTTPException
app = FastAPI()
users_db = {1: "Alice", 2: "Bob"}
@app.get("/users/{user_id}")
def get_user(user_id: int):
if user_id not in users_db:
raise HTTPException(status_code=404, detail=f"User {user_id} not found")
return {"id": user_id, "name": users_db[user_id]}
- Dependency injection:
from fastapi import FastAPI, Depends
app = FastAPI()
def pagination(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
@app.get("/items")
def list_items(p: dict = Depends(pagination)):
return p
@app.get("/users")
def list_users(p: dict = Depends(pagination)):
return p
Что дальше
Освоили основы FastAPI. Тот же Depends дальше используется для аутентификации. В следующем уроке - Pydantic: validation library, на которой построена type-safe валидация в FastAPI. Углубимся в кастомные валидаторы, settings management и сериализацию.