Files
runners-calendar/docs/backend-api-for-frontend.md
Vaka.pro 007d899721
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
chore: drop agent/plan docs, unify .env for Docker stack
- Remove PLAN/agent instruction files; single root .env.example for DB + API
- Stack compose uses env_file .env; delete stack env example duplicate
- Refresh README, backend docs, API doc; trim gitignore/dockerignore

Made-with: Cursor
2026-04-07 00:30:29 +03:00

241 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Backend API — шпаргалка для фронтенда
## 1. Base URL
```
VITE_API_BASE_URL=http://localhost:3001
```
В коде SPA: `import.meta.env.VITE_API_BASE_URL`.
В Docker-стеке из репозитория образ фронта собирается с **`VITE_API_BASE_URL=/api`**: запросы идут на тот же origin, nginx проксирует `/api` на backend (см. `docker/nginx.frontend.conf`).
## 2. CORS
В dev-режиме бэкенд ожидает переменную:
```
CORS_ORIGIN=http://localhost:5173
```
Разрешены методы `GET`, `POST`, `PATCH`, `DELETE` и заголовок `Content-Type: application/json`.
## 3. Эндпоинты
### `GET /health`
Liveness-проверка (без обращения к БД).
**Ответ 200:**
```json
{ "status": "ok" }
```
---
### `GET /ready`
Readiness-проверка (проверяет подключение к БД).
**Ответ 200:**
```json
{ "status": "ready", "db": "connected" }
```
**Ответ 503:**
```json
{ "error": "database_unavailable", "db": "disconnected" }
```
---
### `GET /races`
Список забегов, отсортированных по дате.
**Query-параметры (опциональные):**
| Параметр | Тип | Описание |
|---|---|---|
| `year` | number | Фильтр по году (напр. `2026`) |
| `month` | number | Фильтр по месяцу (112) |
- Без параметров — возвращает все забеги.
- Можно указать только `year`, только `month` или оба.
- `month` без `year` фильтрует по месяцу **всех** лет.
**Пример запроса:**
```
GET /races?year=2026&month=5
```
**Ответ 200:**
```json
[
{
"id": "2026-05-03-kazanskii-marafon",
"date": "2026-05-03",
"title": "Казанский марафон",
"distanceKm": 42.195,
"status": "planned",
"officialUrl": null,
"startTime": null,
"clusterSchedule": null,
"bibPickup": null,
"bibNumber": null,
"finishTime": null,
"finishPlace": null,
"notes": null,
"createdAt": "2026-03-31T12:00:00.000Z",
"updatedAt": null
}
]
```
---
### `GET /races/:id`
Одна запись по `id`.
**Ответ 200:** объект `Race` (см. модель ниже).
**Ответ 404:** тело JSON, поле `details` — массив пояснений (можно показывать в UI или игнорировать).
```json
{ "error": "not_found", "details": ["Race not found"] }
```
---
### `POST /races`
Создание забега.
**Тело запроса (JSON):**
```json
{
"id": "2026-06-01-my-race",
"date": "2026-06-01",
"title": "Мой забег",
"distanceKm": 10,
"status": "planned",
"officialUrl": "https://example.com",
"startTime": "09:30",
"clusterSchedule": null,
"bibPickup": null,
"bibNumber": null,
"finishTime": null,
"finishPlace": null,
"notes": null
}
```
**Обязательные поля:** `id`, `date`, `title`, `distanceKm`.
**Ответ 201:** созданный объект `Race`.
**Ответ 400:**
```json
{ "error": "validation_error", "details": ["Fields id, date, title, distanceKm are required"] }
```
**Ответ 409:**
```json
{ "error": "conflict", "details": ["Race with this id already exists"] }
```
---
### `PATCH /races/:id`
Частичное обновление — передавать **только** изменяемые поля.
**Тело запроса (JSON):**
```json
{
"finishTime": "1:45:30",
"finishPlace": "12/340",
"bibNumber": "1234",
"status": "completed"
}
```
**Допустимые поля:** `date`, `title`, `distanceKm`, `status`, `officialUrl`, `startTime`, `clusterSchedule`, `bibPickup`, `bibNumber`, `finishTime`, `finishPlace`, `notes`.
**Ответ 200:** обновлённый объект `Race`.
**Ответ 400:**
```json
{ "error": "validation_error", "details": ["No updatable fields provided"] }
```
**Ответ 404:**
```json
{ "error": "not_found", "details": ["Race not found"] }
```
---
### `DELETE /races/:id`
Удаление забега.
**Ответ 204:** пустое тело.
**Ответ 404:**
```json
{ "error": "not_found", "details": ["Race not found"] }
```
## 4. Модель `Race` (camelCase)
| Поле | Тип | POST обяз. | PATCH | Описание |
|---|---|---|---|---|
| `id` | string | да | — | Стабильный ключ, напр. `2026-05-03-kazan-marathon` |
| `date` | string | да | да | `YYYY-MM-DD` |
| `title` | string | да | да | Название забега |
| `distanceKm` | number | да | да | Дистанция в км |
| `status` | string \| null | нет | да | `"planned"` / `"registered"` / `"completed"` |
| `officialUrl` | string \| null | нет | да | URL организатора |
| `startTime` | string \| null | нет | да | Время старта, напр. `"09:30"` |
| `clusterSchedule` | string \| null | нет | да | Расписание кластеров |
| `bibPickup` | string \| null | нет | да | Выдача номеров |
| `bibNumber` | string \| null | нет | да | Стартовый номер |
| `finishTime` | string \| null | нет | да | Финишное время `H:MM:SS` или `MM:SS` |
| `finishPlace` | string \| null | нет | да | Место на финише (произвольная строка) |
| `notes` | string \| null | нет | да | Заметки |
| `createdAt` | string | — | — | ISO timestamp (read-only) |
| `updatedAt` | string \| null | — | — | ISO timestamp (read-only) |
## 5. Фильтрация списка (`GET /races`)
- **`year`** — целое число, фильтрует по `EXTRACT(YEAR FROM race_date)`.
- **`month`** — целое число 112, фильтрует по `EXTRACT(MONTH FROM race_date)`.
- Параметры можно комбинировать (`?year=2026&month=5`) или указывать по одному.
- Без параметров — все забеги без ограничения (пагинация пока не реализована).
- Если по фильтру нет результатов — пустой массив `[]`, статус `200`.
## 6. Идемпотентность seed
Seed-скрипт (`npm run seed` в `backend/`) выполняет **upsert** по полю `id` (`INSERT … ON CONFLICT (id) DO UPDATE`). Источник данных — `import/races_2026_calendar.csv` из корня репозитория. Повторный запуск безопасен.
## 7. Поведение при недоступной БД
- `GET /health` — всегда `200` (не проверяет БД).
- `GET /ready` — при недоступной БД: `503 { "error": "database_unavailable", "db": "disconnected" }`. В режиме **`CALENDAR_RUN_MOCK_DB`** (dev/CI без Postgres) readiness возвращает успех без реального подключения — см. `docs/backend.md`.
- Все остальные маршруты — `503 { "error": "database_unavailable" }`.
- В логах сервера: строка ошибки с контекстом маршрута.