Часті питання про командну співпрацю: контракти, конфігурація й тестові дані
Головне: Стиль найменування, формати часу, семантика null, форми переліків, формати конфігурації, організація тестових даних і конфлікти злиття — дванадцять компромісів, про які команди справді сперечаються.

Коротко: закріпіть це один раз у конвенції команди — обгортка {"data":…,"meta":…} із кодами статусу HTTP, один стиль найменування в усьому проєкті, часові мітки RFC 3339 з часовим поясом і рядкові переліки — потім еволюціонуйте кожне нове API проти того самого контракту, лише додаючи поля, ніколи не змінюючи чи видаляючи їх.
На ці питання немає єдиної правильної відповіді, але команда мусить мати єдину відповідь — інакше кожне нове API починає ту саму суперечку заново.
1. Контракти API
Яку структуру має повертати API?
Три поширені форми, кожна з ціною:
| Форма | Приклад | Переваги | Недоліки |
|---|---|---|---|
| Сирі дані | [{...}, {...}] |
Просто | Не може нести метадані |
| Фіксована обгортка | {"code":0,"message":"ok","data":{...}} |
Уніфікована обробка помилок | Бізнес-код дублює статус HTTP |
| Обгортка ресурсу | {"data":{...},"meta":{...},"links":{...}} |
Розширювана, ясна семантика | Ще один рівень вкладеності |
Для внутрішніх бізнес-API надавайте перевагу «обгортці ресурсу + кодам статусу HTTP, що виражають результат»:
{
"data": [{ "id": "1", "name": "示例" }],
"meta": { "total": 128, "page": 2, "pageSize": 20 }
}
Для помилок використовуйте код статусу HTTP плюс структуроване тіло помилки, а не вкладайте ще один code:
{
"error": {
"type": "validation_failed",
"message": "请求参数校验失败",
"details": [
{ "path": "/email", "reason": "格式不正确" }
]
}
}
Ключовий момент: коди статусу HTTP уже виражають успіх чи збій, тож бізнес-код зайвий. Коли дві системи співіснують, неузгодженості на кшталт «201, але code 500» майже неминучі.
Уникайте голих масивів. З масивом на верхньому рівні ви не можете долучити пагінацію, підсумки чи курсори, а додавання їх пізніше означає зміну кожного викликача.
Чи мають назви полів використовувати snake_case чи camelCase?
Оберіть одне, застосуйте в усьому проєкті й ніколи не змішуйте їх в одному документі JSON.
| Стиль | Поширена екосистема | Приклад |
|---|---|---|
camelCase |
JavaScript / TypeScript / Java | userName, createdAt |
snake_case |
Python / Ruby / Go (частково) / стовпці баз даних | user_name, created_at |
kebab-case |
Рідко; ключі з дефісами незручні для доступу | user-name |
Критерії:
- Хто споживає цей JSON: обирайте camelCase, коли домінує фронтенд, snake_case, коли домінують конвеєри даних і сервіси Python;
- Чи відображається він прямо на стовпці бази даних: snake_case заощаджує шар перетворення, коли структури таблиць передають напряму;
- Наступність наявних систем: тримайте те, що використовує наявний API; узгодженість краще за правильність.
Якщо справді треба конвертувати, робіть це на межі (шлюз або шар запитів фронтенду), а не розкидайте toCamelCase по бізнес-коду.
Який формат мають використовувати поля часу?
Використовуйте рядки RFC 3339 / ISO 8601 зі зміщенням часового поясу:
{
"createdAt": "2026-10-30T11:37:57+08:00",
"updatedAt": "2026-10-30T03:37:57Z"
}
Ключові моменти:
- Включайте часовий пояс:
2026-10-30 11:37:57без поясу неминуче зламається в сценаріях між часовими поясами; - Використовуйте
Zабо зміщення послідовно: обидва правильні, але не змішуйте їх в одному API; - Уникайте числових часових міток: чи це секунди чи мілісекунди й чи включені високосні секунди — кожна команда мусить перетлумачити це. Коли потрібна компактність, використовуйте ціле в мілісекундах, але закріпіть одиницю в документації.
Надавайте перевагу назвам полів із суфіксом At, як-от createdAt / updatedAt / deletedAt,
щоб одразу було видно, що значення — момент часу, а не тривалість. Для тривалостей використовуйте timeoutMs, ttlSeconds — кладіть одиницю в назву.
Мають nulls бути null чи пропущеним полем?
Залежить від того, чи «це поле має сенс»:
| Семантика | Форма | Примітки |
|---|---|---|
| Поле існує, але ще не має значення | "name": null |
Явно каже «перевірено, і воно порожнє» |
| Поле не стосується цього ресурсу | Пропустити | Наприклад, звичайний користувач не повинен мати vipExpireAt |
| Часткове оновлення (PATCH) | Пропуск = не змінювати | Лише null тут означає «очистити» |
Третій випадок найчастіше проґавлюють. У запиті PATCH:
{ "nickname": null }
це означає «очистити псевдонім», тоді як залишення nickname поза тілом запиту означає «не торкатися псевдоніма».
Якщо трактувати обидва як «очистити», користувач, змінюючи email, стере свій псевдонім.
Ще раз: не використовуйте undefined. Це не валідне значення JSON, і JSON.stringify цілком відкидає ключ,
тож поведінка збігається з «пропустити», тоді як код виглядає як «порожнє значення було передано явно».
Як найменувати булеві поля?
Використовуйте префікси is / has / can / should і уникайте заперечень.
{
"isActive": true,
"hasPassword": false,
"canEdit": true
}
Три уроки:
- Без заперечень:
isDisabled: falseзмушує читача один раз інвертувати його;isEnabled: trueбезпосередніше. Подвійне заперечення (isNotDisabled) — катастрофа; - Не використовуйте числа для булевих значень:
"enabled": 1змушує кожного споживача один раз написати=== 1; - Не використовуйте рядки:
"enable": "true"є істинним у JavaScript, іif (data.enable)також істинне для"false"; такі баги вкрай важко простежити.
Мають поля-переліки використовувати числа чи рядки?
Використовуйте рядки, якщо немає чіткого обмеження зберігання чи пропускної здатності.
{ "status": "active" }
Порівняно з { "status": 1 }, рядки виграють, бо:
- Логи, перехоплення пакетів і бази даних безпосередньо читабельні без пошуку в таблиці відображення;
- Додавання значення переліку не змінює сенс наявних значень (вставлення значення в середину числового переліку зсуває все);
- Фронтенд не потребує синхронної підтримки відображення
1 → 'active'.
Ціна — кілька зайвих байтів, що після стиснення майже нехтовно.
Супутня практика: використовуйте нижній регістр і підкреслення для рядкових значень переліку (pending_review) і обмежуйте їх через enum у Schema,
щоб невалідні значення відхилялися на етапі перевірки.
2. Конфігурація й тестові дані
Має конфігурація бути JSON, YAML чи TOML?
Обирайте за призначенням, а не за вподобанням:
| Формат | Добре для | Погано для |
|---|---|---|
| JSON | Програмного читання й запису, строго структурованих даних, перевірки Schema | Частого ручного редагування (коментарі не дозволені) |
| YAML | Глибоко вкладеної конфігурації розгортання (K8s) | Рукописні відступи схильні до помилок; yes/no стають булевими |
| TOML | Пласкої конфігурації залежностей та інструментів (Cargo.toml, pyproject.toml) |
Поганої читабельності глибокої вкладеності |
| JSONC | Конфігурації редактора, що потребує коментарів (tsconfig.json) |
Перетинання меж систем |
Поширена помилка в командах — зберігати бізнес-конфігурацію в YAML — неявне перетворення типів YAML (1.0 стає числом,
NO стає булевим, 2026-10-30 стає датою) дає різні результати в різних парсерах.
Якщо конфігурацію треба строго перевіряти програмою, обирайте JSON + Schema і документуйте коментарі через поле _comment або окрему документацію.
Як організовувати тестові дані?
Організовуйте за принципом «один базовий зразок + мінімальні відмінності», а не копіюйте величезний JSON для кожного тест-кейсу.
fixtures/
├─ users/
│ ├─ base.json # Базовий обʼєкт користувача
│ ├─ no-email.json # Бракує лише email
│ └─ invalid-age.json # Лише age поза діапазоном
└─ fixtures.ts # Службові функції для завантаження й поверхневого злиття
import baseUser from './users/base.json';
export function makeUser(overrides: Partial<User> = {}): User {
return { ...baseUser, ...overrides };
}
Так новому тест-кейсу треба написати лише відмінні поля, а зміна базового поля автоматично оновлює кожен випадок.
Три правила:
- Тестові дані мусять проходити перевірку Schema: базовий зразок сам має бути валідними даними;
- Не експортуйте реальні дані з виробництва: номери телефонів, номери ID і адреси — усе це персональна інформація;
- Тестові дані належать під контроль версій, але мусять лишатися малими: один файл понад кілька десятків КБ вартий обрізання чи генерації.
Як скоротити конфлікти злиття у файлах JSON?
JSON конфліктує легко: форматувальник переливає весь файл одразу, тож двоє людей, кожен змінюючи один рядок, конфліктують на всьому файлі.
Чотири тактики:
- Уніфікуйте форматування й долучіть його до
.editorconfig: узгоджені відступи, закінчення рядків і завершальне переведення рядка уникають нерелевантних diff; - Сортуйте ключі: домовтеся про абетковий порядок, щоб нове поле не зіштовхувалося з точкою вставлення іншого;
- Розділіть файли: розбийте один гігантський
config.jsonна кілька файлів за доменом, і поверхня конфлікту природно зменшиться; - Уникайте комітів «переформатувати все»: відокремте форматування від бізнес-змін у два коміти, щоб рев'ю лишалося читабельним.
Якщо команда використовує lock-файли (pnpm-lock.yaml, package-lock.json), не зливайте їх вручну; натомість перегенеруйте:
git checkout --theirs pnpm-lock.yaml
pnpm install
Як підтримувати сумісність версій?
Принцип — «додавати, не змінювати, не видаляти».
| Зміна | Сумісність | Підхід |
|---|---|---|
| Додати необовʼязкове поле | Сумісна | Додайте; споживачі ігнорують невідомі поля |
| Додати обовʼязкове поле | Несумісна | Підніміть версію або надайте значення за замовчуванням |
| Видалити поле | Несумісна | Спершу позначте застарілим, стежте за викликачами, потім видаляйте |
| Змінити тип поля | Несумісна | Додайте нову назву поля й подвійний запис під час переходу |
| Змінити набір значень переліку | Залежить | Додавання значень сумісне; видалення — ні |
Механізми підтримки:
- Споживачі використовують «поблажливий розбір»: ігнорують невідомі поля й надають типові для відсутніх;
- Сервер якийсь час тримає старі поля й повертає заголовок
Deprecationу відповіді як підказку; - У Schema не позначайте нові поля як
required, а застарілі позначайтеdeprecated: true.
Як перевіряти контракти в CI?
Трактуйте Schema як частину коду й перевіряйте її при кожному коміті.
# Перевірте зразки даних за Schema (через ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false
Запускайте в CI три речі:
- Валідність самої Schema: чи оголошення
$schemaвалідне й чи розвʼязується$ref; - Чи відповідають тестові дані Schema: гарантує, що тестові дані не дрейфують;
- Чи контракт зворотно сумісний: порівняйте з попередньою версією й перевірте на видалені поля чи змінені типи.
Третій пункт потребує інструментарію (як-от openapi-diff чи json-schema-diff);
без нього принаймні порівняйте зміни в масивах required і наборах ключів properties скриптом.
3. Стислий довідник
| Питання | Рекомендація |
|---|---|
| Структура відповіді | {"data":..., "meta":...}, з кодами статусу HTTP для помилок |
| Стиль найменування | camelCase (фронтенд на першому місці) або snake_case (дані на першому місці), однаково в усьому проєкті |
| Формат часу | Рядки RFC 3339 з часовим поясом, назви полів з At у кінці |
| Nulls | Використовуйте null для відсутності значення, пропускайте нерелевантні поля, пропуск у PATCH означає «не змінювати» |
| Найменування булевих | Префікси is/has/can, без заперечень, без 0/1 |
| Переліки | Рядки, нижній регістр із підкресленнями, додайте enum у Schema |
| Формат конфігурації | JSON для програмної перевірки, TOML або JSONC для ручного редагування |
| Тестові дані | Один базовий зразок + перевизначення з мінімальними відмінностями, валідні за Schema |
| Конфлікти злиття | Уніфікуйте форматування, сортуйте ключі, розділяйте файли |
| Сумісність версій | Додавайте, не змінюйте, не видаляйте; позначайте застарілим перед видаленням |