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 UI
  • http://localhost:8000/redoc - ReDoc альтернативный UI
  • http://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-Agentuser_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)

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

Структура FastAPI: myapp с main.py, под-пакетами routers (users, orders, auth) и models (user, order), плюс deps.py для общих Depends

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 разработки.

Мини-задание

  1. Минимальный 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
  1. С обработкой ошибок:
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]}
  1. 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 и сериализацию.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.