Files
runners-calendar/docs/backend-api-for-frontend.md
Anton 698ae37553 feat(backend): implement REST API for races calendar
Express + TypeScript backend with PostgreSQL: CRUD endpoints for /races (GET list with year/month filters, GET by id, POST, PATCH, DELETE), health/readiness probes, SQL migration runner, seed script with upsert from CSV, camelCase/snake_case mapper, CORS, env validation, docker-compose, and API docs for frontend.

Made-with: Cursor
2026-04-01 14:47:53 +03:00

235 lines
6.3 KiB
Markdown
Raw Permalink 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`.
## 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,
"notes": null,
"createdAt": "2026-03-31T12:00:00.000Z",
"updatedAt": null
}
]
```
---
### `GET /races/:id`
Одна запись по `id`.
**Ответ 200:** объект `Race` (см. модель ниже).
**Ответ 404:**
```json
{ "error": "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,
"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",
"bibNumber": "1234",
"status": "completed"
}
```
**Допустимые поля:** `date`, `title`, `distanceKm`, `status`, `officialUrl`, `startTime`, `clusterSchedule`, `bibPickup`, `bibNumber`, `finishTime`, `notes`.
**Ответ 200:** обновлённый объект `Race`.
**Ответ 400:**
```json
{ "error": "validation_error", "details": ["No updatable fields provided"] }
```
**Ответ 404:**
```json
{ "error": "not_found" }
```
---
### `DELETE /races/:id`
Удаление забега.
**Ответ 204:** пустое тело.
**Ответ 404:**
```json
{ "error": "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"` / `"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` |
| `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" }`.
- Все остальные маршруты — `503 { "error": "database_unavailable" }`.
- В логах сервера: строка ошибки с контекстом маршрута.