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
6.3 KiB
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:
{ "status": "ok" }
GET /ready
Readiness-проверка (проверяет подключение к БД).
Ответ 200:
{ "status": "ready", "db": "connected" }
Ответ 503:
{ "error": "database_unavailable", "db": "disconnected" }
GET /races
Список забегов, отсортированных по дате.
Query-параметры (опциональные):
| Параметр | Тип | Описание |
|---|---|---|
year |
number | Фильтр по году (напр. 2026) |
month |
number | Фильтр по месяцу (1–12) |
- Без параметров — возвращает все забеги.
- Можно указать только
year, толькоmonthили оба. monthбезyearфильтрует по месяцу всех лет.
Пример запроса:
GET /races?year=2026&month=5
Ответ 200:
[
{
"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:
{ "error": "not_found" }
POST /races
Создание забега.
Тело запроса (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:
{ "error": "validation_error", "details": ["Fields id, date, title, distanceKm are required"] }
Ответ 409:
{ "error": "conflict", "details": ["Race with this id already exists"] }
PATCH /races/:id
Частичное обновление — передавать только изменяемые поля.
Тело запроса (JSON):
{
"finishTime": "1:45:30",
"bibNumber": "1234",
"status": "completed"
}
Допустимые поля: date, title, distanceKm, status, officialUrl, startTime, clusterSchedule, bibPickup, bibNumber, finishTime, notes.
Ответ 200: обновлённый объект Race.
Ответ 400:
{ "error": "validation_error", "details": ["No updatable fields provided"] }
Ответ 404:
{ "error": "not_found" }
DELETE /races/:id
Удаление забега.
Ответ 204: пустое тело.
Ответ 404:
{ "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— целое число 1–12, фильтрует по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" }. - В логах сервера: строка ошибки с контекстом маршрута.