Опирается на правила:
R-QRY-1..9иR-QRY-X1..X5из REST API Style Guide → раздел Query-параметры и пагинация.
Важно знать
- camelCase для имён параметров — через
Query(alias="pageSize").- Фильтрация —
Query(alias="customerId"),Query(alias="status").- Диапазоны —
Query(alias="dateFrom")/Query(alias="dateTo").- Offset-based —
page(1-based!) +size.- Cursor-based —
cursor(непрозрачный токен) +size. Клиент не парсит.- Множественные значения —
list[str] = Query(...)(повтор, не CSV).- Сложные запросы —
POST /resources/searchс Pydantic-телом.
FastAPI code-first: query-параметры описываются в сигнатуре функции с Query(...). Имена аргументов функции — snake_case (PEP 8), но в URL нужен camelCase. Решение — Query(alias="camelCase").
Имена параметров
R-QRY-1: camelCase через alias.
from fastapi import APIRouter, Query
router = APIRouter(prefix="/api/v1", redirect_slashes=False)
@router.get("/orders")
async def get_orders(
customer_id: str | None = Query(default=None, alias="customerId"),
date_from: str | None = Query(default=None, alias="dateFrom"),
date_to: str | None = Query(default=None, alias="dateTo"),
):
...
GET /api/v1/orders?customerId=123&dateFrom=2026-01-01 ✓
GET /api/v1/orders?customer_id=123 ✗ — snake_case
GET /api/v1/orders?CustomerID=123 ✗ — PascalCase
Фильтрация и диапазоны
R-QRY-2..3:
from enum import StrEnum
class OrderStatus(StrEnum):
CREATED = "CREATED"
CONFIRMED = "CONFIRMED"
SHIPPED = "SHIPPED"
@router.get("/orders")
async def get_orders(
status: OrderStatus | None = Query(default=None),
customer_id: str | None = Query(default=None, alias="customerId"),
date_from: str | None = Query(default=None, alias="dateFrom"),
date_to: str | None = Query(default=None, alias="dateTo"),
amount_from: float | None = Query(default=None, alias="amountFrom"),
amount_to: float | None = Query(default=None, alias="amountTo"),
):
...
From/To — инклюзивный интервал [from, to].
Offset-based пагинация
R-QRY-4: page (1-based) + size.
@router.get("/orders")
async def get_orders(
page: int = Query(default=1, ge=1, alias="page"),
size: int = Query(default=20, ge=1, le=100, alias="size"),
):
offset = (page - 1) * size
...
Контракт API — 1-based. page=1 — первая страница.
Формат ответа:
from pydantic import BaseModel
class PaginatedOrders(BaseModel):
content: list[OrderResponse]
page: int
size: int
total_elements: int
total_pages: int
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
{
"content": [...],
"page": 1,
"size": 20,
"totalElements": 243,
"totalPages": 13
}
R-QRY-X2: page=0 в публичном контракте — запрещено.
Cursor-based пагинация
R-QRY-5: cursor (opaque) + size.
import base64, json
@router.get("/orders")
async def get_orders(
size: int = Query(default=20, ge=1, le=100),
cursor: str | None = Query(default=None),
):
decoded = None
if cursor:
decoded = json.loads(base64.b64decode(cursor))
...
def encode_cursor(last_id: str, last_created_at: str) -> str:
payload = {"id": last_id, "createdAt": last_created_at}
return base64.b64encode(json.dumps(payload).encode()).decode()
class CursorPage(BaseModel):
content: list[OrderResponse]
size: int
next_cursor: str | None = None
prev_cursor: str | None = None
has_next: bool
has_prev: bool
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
Клиент передаёт cursor из поля nextCursor предыдущего ответа — не парсит и не конструирует его содержимое. R-QRY-X5: парсинг cursor на клиенте — запрещено.
Сортировка
R-QRY-6: sort=field,direction.
@router.get("/orders")
async def get_orders(
sort: list[str] = Query(default=["createdAt,desc"]),
):
...
GET /api/v1/orders?sort=createdAt,desc
GET /api/v1/orders?sort=totalAmount,asc&sort=createdAt,desc
Полнотекстовый поиск
R-QRY-7: параметр q.
@router.get("/products")
async def search_products(q: str | None = Query(default=None)):
...
Множественные значения
R-QRY-8: повтор параметра через list[T] = Query(...).
@router.get("/orders")
async def get_orders(
status: list[OrderStatus] = Query(default=[]),
):
...
FastAPI автоматически генерирует style: form, explode: true в OpenAPI — повтор параметра:
GET /api/v1/orders?status=CREATED&status=CONFIRMED ✓
GET /api/v1/orders?status=CREATED,CONFIRMED ✗ — comma-separated
R-QRY-X3: comma-separated — запрещено. Требует ручного парсинга, ломается при запятой в значении, не соответствует умолчанию OpenAPI.
POST /resources/search
R-QRY-9: для сложных запросов.
from pydantic import BaseModel
class OrderDateRange(BaseModel):
from_: str | None = Field(default=None, alias="from")
to: str | None = None
class CustomerFilter(BaseModel):
region_ids: list[int] = Field(default=[], alias="regionIds")
segment: str | None = None
class SortField(BaseModel):
field: str
direction: str = "DESC"
class OrderSearchRequest(BaseModel):
statuses: list[OrderStatus] = []
date_range: OrderDateRange | None = Field(default=None, alias="dateRange")
customer: CustomerFilter | None = None
sort: list[SortField] = Field(default=[SortField(field="createdAt")])
page: int = 1
size: int = 20
model_config = ConfigDict(populate_by_name=True)
@router.post(
"/orders/search",
status_code=200,
operation_id="searchOrders",
tags=["Orders"],
summary="Поиск заказов",
response_model=PaginatedOrders,
response_model_exclude_none=True,
)
async def search_orders(body: OrderSearchRequest) -> PaginatedOrders:
...
Критерии перехода с GET на POST /search:
| Ситуация | Что выбрать |
|---|---|
| Плоские поля (status, date) | GET |
| Вложенные объекты | POST /search |
| Массив 10+ значений в одном фильтре | POST /search |
| AND/OR-комбинации | POST /search |
| Запрос нужно сохранять | POST /search |
URL: /resources/search (не /query, не /find). Ответ — 200 OK, формат тот же что GET /resources.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
?customer_id= snake_case | R-QRY-X1 | Query(alias="customerId") |
page=0 (0-based в контракте) | R-QRY-X2 | page=1 |
?status=A,B comma-separated | R-QRY-X3 | ?status=A&status=B |
?action=cancel бизнес-логика в query | R-QRY-X4 | POST /orders/{id}/cancel |
| Клиент декодирует cursor | R-QRY-X5 | opaque token |
| GET search с длинным URL | R-QRY-9 | POST /search |
int без alias для page_size | R-QRY-1 | Query(alias="pageSize") |
Куда дальше
- REST API → Query-параметры (нормативно) — формулировки.
- URL и ресурсы — формат пути.
- JSON и формат ответов —
content+ пагинация,model_config. - Заголовки и трассировка —
Idempotency-Keyдля POST /search. - Batch, async, локализация — async-поиск.