5.9 KiB
Backend API — контракт для frontend
Base URL и авторизация
SPA обращается к относительному префиксу /api. В dev Vite проксирует его на
http://localhost:3001/api; в Docker nginx проксирует на
runners-calendar-backend:3000.
Все маршруты /api/races требуют сессионную cookie авторизованного пользователя
с подтверждённым email. Записи принадлежат текущему пользователю: чужая запись
не видна и отвечает 404.
После POST /api/auth/login или GET /api/auth/me клиент получает
csrfToken. Для каждого POST, PATCH и DELETE с активной сессией его нужно
передавать в заголовке X-CSRF-Token. Cookie передаётся браузером автоматически;
при отдельном origin запрос должен использовать credentials.
Служебные маршруты
GET /api/health
Liveness без проверки БД:
{ "status": "ok", "version": "<backend version>" }
GET /api/meta
Версия для footer:
{ "version": "<backend version>" }
GET /api/ready
Проверяет БД. Успех: { "status": "ready", "db": "connected" }.
Недоступная БД: 503 { "error": "database_unavailable", "db": "disconnected" }.
Забеги
GET /api/races
Возвращает только забеги текущего пользователя, отсортированные по дате. Опциональные
query-параметры: year (целое число) и month (1–12); без результатов возвращается
[].
GET /api/races/:id
Возвращает один забег. id — UUID, который создаёт сервер. Не путайте его с slug:
slug создаёт клиент как читаемый ключ и он уникален в рамках одного пользователя.
POST /api/races
Создаёт забег. Обязательные поля: slug, date, title, distanceKm.
Неизвестные поля отклоняются.
{
"slug": "2026-06-01-my-race",
"date": "2026-06-01",
"title": "Мой забег",
"distanceKm": 10,
"status": "planned",
"officialUrl": "https://example.com",
"startTime": "09:30"
}
Успех: 201 и объект Race. Одинаковый slug у того же пользователя: 409
{ "error": "conflict", "details": ["Race with this slug already exists"] }.
Если coverImageUrl не передан, backend может найти Open Graph-обложку по
officialUrl; это best-effort и не мешает созданию при ошибке внешнего сайта.
PATCH /api/races/:id
Частично обновляет забег. Передайте хотя бы одно поле из модели ниже; неизвестные
поля и пустой объект отклоняются. Успех: 200 и обновлённый Race.
DELETE /api/races/:id
Удаляет забег. Успех: 204 без тела.
Модель Race
| Поле | Тип | POST | PATCH | Ограничение |
|---|---|---|---|---|
id |
UUID | сервер | — | Read-only UUID сервера |
slug |
string | да | да | 1–120 символов: буквы/цифры, разделённые - |
date |
string | да | да | Реальная дата YYYY-MM-DD |
title |
string | да | да | 1–200 символов |
distanceKm |
number | да | да | Больше 0, не более 999.999 |
status |
string/null | нет | да | planned, registered или completed |
officialUrl |
HTTP(S) URL/null | нет | да | До 2048 символов |
coverImageUrl |
HTTP(S) URL/null | нет | да | До 2048 символов |
startTime |
string/null | нет | да | HH:MM или HH:MM:SS |
clusterSchedule |
string/null | нет | да | До 1000 символов |
bibPickup |
string/null | нет | да | До 500 символов |
bibNumber |
string/null | нет | да | До 100 символов |
finishTime |
string/null | нет | да | SS, MM:SS или H:MM:SS |
finishPlace |
string/null | нет | да | До 100 символов |
notes |
string/null | нет | да | До 5000 символов |
createdAt |
ISO datetime | сервер | — | Read-only |
updatedAt |
ISO datetime/null | сервер | — | Read-only |
Ошибки
| Статус | Тело | Когда |
|---|---|---|
| 400 | { "error": "validation_error", "details": ["…"] } |
Неверный UUID, query или тело запроса |
| 401 | { "error": "unauthorized", "details": ["Authentication required"] } |
Нет сессии |
| 403 | csrf_error / email_not_verified |
Неверный CSRF-токен или email не подтверждён |
| 404 | { "error": "not_found", "details": ["Race not found"] } |
Забега нет либо он принадлежит другому пользователю |
| 409 | conflict |
Дублирующий slug пользователя |
| 500 | unknown_error |
Непредвиденная ошибка приложения |
| 503 | database_unavailable |
База недоступна |
Некорректный JSON также отвечает 400 validation_error.
Seed
npm run seed выполняет upsert по slug и не перезаписывает пользовательские
изменения. После включения авторизации ему нужен SEED_OWNER_USER_ID или
SEED_OWNER_EMAIL.