Repository navigation
API Contract Guide
Эта страница — для тех, кто никогда раньше не работал с API. Если вы уже писали fetch() или requests.get() и понимаете, что такое JSON и HTTP-статусы — можете сразу перейти к разделу «Как задокументировать один эндпоинт».
Если нет — читайте по порядку, здесь всё объясняется с нуля.
API (Application Programming Interface) в контексте этого проекта — это просто способ, которым страница в браузере (frontend) «разговаривает» с программой на сервере (backend), чтобы получить или сохранить данные.
Разговор устроен так:
- Frontend отправляет запрос (request) на определённый адрес backend'а. Например: «дай мне список оценок студента с id=5».
- Backend получает этот запрос, лезет в базу данных, находит нужное, и отправляет ответ (response).
- Frontend получает ответ и рисует его на странице (таблицу, список, сообщение об ошибке — что угодно).
Технически это HTTP-запрос — то же самое, что происходит, когда браузер открывает обычную веб-страницу, только вместо HTML в ответ обычно приходит JSON (текстовый формат для передачи структурированных данных, похожий на Python-словарь):
{
"id": 5,
"name": "Иванов Иван",
"grades": [4, 5, 3]
}В нашем проекте это выглядит конкретно так: JS-код на странице (frontend/<модуль>/...) делает fetch("/api/v1/<модуль>/..."), а на другом конце этот запрос принимает Python-код (modules/<модуль>/router.py), который вы (если вы в backend-команде) и пишете.
У каждого запроса есть:
-
Метод — что мы хотим сделать:
GET(получить данные),POST(создать что-то новое),PUT/PATCH(изменить существующее),DELETE(удалить). -
Путь (path) — адрес, например
/api/v1/gradebook/students/5/grades. - Иногда — тело запроса (request body) — данные, которые frontend отправляет backend'у (например, при
POST: «вот новая оценка, сохрани её»). -
Статус ответа — число вроде
200(всё хорошо),201(успешно создано),404(не найдено),422(данные не прошли проверку),500(ошибка на сервере). - Тело ответа (response body) — данные, которые backend возвращает.
Мало того, что backend и frontend вообще «разговаривают» через API — они должны заранее договориться, как именно выглядит этот разговор, потому что каждая сторона пишет код независимо, глядя только на свою половину.
Представим (чисто гипотетически, не про реальный модуль) — backend-команда решила, что для получения оценок студента нужен запрос:
GET /api/v1/example/items?student_id=5
— то есть id передаётся как query-параметр (после ?). А frontend-команда, ничего не согласовав, ожидала:
GET /api/v1/example/items/5
— то есть id передаётся как часть пути. Это два совершенно разных способа, и backend, реализовавший первый вариант, просто не будет отвечать на запросы, которые шлёт frontend — оба будут получать 404 Not Found, хотя каждая команда по отдельности всё сделала «правильно».
Второй частый случай — форма ответа. Backend может вернуть просто список:
[{"id": 1, "text": "..."}, {"id": 2, "text": "..."}]а frontend-код написан в расчёте на то, что список лежит внутри объекта:
{"items": [{"id": 1, "text": "..."}, {"id": 2, "text": "..."}]}Код, который делает data.items.forEach(...), упадёт с ошибкой на первом варианте, а код, который делает data.forEach(...), упадёт на втором. И то, и другое — совершенно нормальный, рабочий код, если бы стороны знали заранее, что именно к ним прилетит.
API-контракт — это письменная договорённость о том, как именно выглядит каждый такой обмен, зафиксированная до того, как обе команды начали писать код, а не выясненная в последний день перед защитой методом «почему у меня ничего не работает».
Именно поэтому в этом проекте у backend- и frontend-команды одного модуля — отдельные файлы ТЗ (specification-backend.md и specification-frontend.md, см. Getting Started): раз файлы разные, никто не может «случайно» скопировать раздел про API из чужого файла — обе команды обязаны реально сверить, что они там написали, и именно это сверение и есть работа с контрактом.
К концу Модуля 1 (см. Систему оценивания) backend-команда должна в своём specification-backend.md, в разделе «4.1. API Endpoints», расписать каждый эндпоинт, который будет нужен frontend'у. Для каждого эндпоинта нужно зафиксировать:
-
Метод и путь — например,
GET /api/v1/gradebook/students/{student_id}/grades. -
Параметры пути и запроса — что и в каком формате передаётся (path-параметр в фигурных скобках, query-параметры, тело запроса для
POST/PUT). -
Тело запроса (для
POST/PUT/PATCH) — какие поля, какого типа, какие обязательны. - Тело успешного ответа — точная структура JSON, с примером.
-
Статус успешного ответа —
200дляGET, обычно201дляPOST, создающего запись. -
Возможные ошибки — какой статус и какое тело ответа приходит, если что-то пошло не так (не найдено —
404, невалидные данные —422, и т.д.).
Не нужно уметь всё это писать «по ГОСТу» — просто нужно, чтобы после прочтения этого раздела frontend-команда могла написать fetch()-запрос и знать заранее, что она получит в ответ, не дожидаясь, пока backend реально это напишет.
Backend пишет этот раздел первым, ориентируясь на функциональные требования модуля (что должна уметь делать система) — а не наоборот. Сначала — что нужно фронтенду для экранов модуля (это как раз стоит обсудить с frontend-командой лично, до того как писать раздел), потом — как это оформить в виде эндпоинтов.
Frontend-команде контракт нужен к началу Модуля 2 — то есть к моменту, когда обе команды переходят от «писать ТЗ» к «писать код». Если контракт согласован раньше — тем лучше, можно начинать раньше.
Что именно нужно frontend'у из раздела backend'а «4.1. API Endpoints»:
- Список всех эндпоинтов, которые понадобятся для экранов модуля (форма ввода оценки — значит, нужен
POST; таблица оценок — значит, нуженGET, и т.д.). - Точный формат ответа каждого
GET-эндпоинта — чтобы уже сейчас написать код отрисовки, а не переписывать его потом, когда backend «на самом деле» пришлёт другую структуру. - Формат ошибок — чтобы показать пользователю понятное сообщение, а не «упасть» молча, если, например, студент с таким id не найден.
Если backend ещё физически не готов (эндпоинт ещё не реализован, но уже описан в specification-backend.md) — frontend всё равно может начинать писать код: временно замените реальный fetch() захардкоженным объектом с той же структурой, что описана в контракте (это называется «заглушка», mock), и как только backend выложит рабочий эндпоинт — просто уберите заглушку. Так обе команды могут работать параллельно, а не ждать друг друга.
Если во время работы выясняется, что контракт нужно поменять (frontend'у не хватает какого-то поля, или неудобно передавать id так, как решил backend) — это не повод молча поменять код на своей стороне и понадеяться, что «как-нибудь сойдётся». Договоритесь заново, обновите оба файла ТЗ (у backend — что реально отдаёт эндпоинт, у frontend — что он теперь ожидает), и только после этого меняйте код. Несовпадение, обнаруженное во время сверки, а не во время защиты — это нормальная рабочая ситуация, а не провал.
Ниже — пример того, как должна выглядеть запись про один эндпоинт в разделе «4.1. API Endpoints» вашего specification-backend.md. Это реальный, рабочий эндпоинт из готового примера в репозитории — docs/examples/example_module/ (backend) и frontend/_example/ (frontend, обращается именно к нему). Запустите проект и откройте http://127.0.0.1:8000/frontend/_example/, чтобы увидеть его вживую, или посмотрите код обоих файлов напрямую.
Назначение: получить список всех записей.
Параметры: нет.
Успешный ответ — 200 OK:
[
{
"id": 1,
"text": "Первая запись",
"created_at": "2026-09-18T10:00:00"
},
{
"id": 2,
"text": "Вторая запись",
"created_at": "2026-09-18T10:05:00"
}
](обратите внимание: ответ — «голый» массив, а не объект вида {"items": [...]}. Это тоже часть контракта — если бы это было не так, JS-код на странице был бы написан иначе.)
Назначение: получить одну запись по id.
Параметры пути: item_id (целое число) — id записи.
Успешный ответ — 200 OK:
{
"id": 1,
"text": "Первая запись",
"created_at": "2026-09-18T10:00:00"
}Ошибка — 404 Not Found (записи с таким id нет):
{
"detail": "Не найдено"
}Назначение: создать новую запись.
Тело запроса:
{
"text": "Новая запись"
}| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
text |
строка | да | Содержимое записи |
Успешный ответ — 201 Created:
{
"id": 3,
"text": "Новая запись",
"created_at": "2026-09-18T10:10:00"
}Ошибка — 422 Unprocessable Entity (например, text не передан):
FastAPI формирует такое тело ответа автоматически — просто знайте, что оно будет выглядеть примерно так и что нужно проверять response.ok на frontend, а не считать, что запрос всегда успешен.
Скопируйте эту структуру (метод + путь → назначение → параметры → успешный ответ с примером JSON → возможные ошибки) для каждого реального эндпоинта вашего модуля.
Соберитесь вдвоём — по одному представителю от backend- и frontend-команды одного модуля — и пройдитесь по обоим файлам ТЗ вместе:
- Для каждого действия на экранах frontend (посмотреть список, открыть карточку, сохранить форму, удалить запись и т.д.) в
specification-backend.mdесть соответствующий эндпоинт. - Метод (
GET/POST/...) и путь у каждого эндпоинта в обоих файлах ТЗ записаны одинаково, символ в символ. - Формат id и других параметров — одинаковый способ передачи (путь или query) в обоих файлах.
- Структура успешного ответа (какие поля, вложенный объект или голый массив/значение) — одинаковая в обоих файлах.
- Оговорено, что приходит при ошибке (хотя бы для случая «не найдено» и «невалидные данные»).
Если после этого чек-листа расхождений не осталось — контракт согласован, можно спокойно писать код параллельно, не боясь, что в конце всё придётся переделывать.
-
🚀 Начало работы — шаблоны
specification-backend.mdиspecification-frontend.md. - 📊 Система оценивания — на каком этапе что должно быть готово.
- ❓ FAQ — в том числе про CORS и типичные ошибки frontend↔backend.
-
docs/architecture.mdиdocs/examples/example_module/README.mdв репозитории — как всё это устроено на уровне кода.