Вопросы о командной работе: контракты, конфигурация и фикстуры
Главное: Стиль именования, форматы времени, семантика 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 — помещайте единицу в имя.
Должны ли null быть 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: базовая фикстура сама должна быть валидными данными;
- Не экспортируйте реальные данные из продакшена: телефоны, номера удостоверений и адреса — всё это персональная информация;
- Фикстуры принадлежат системе контроля версий, но должны оставаться небольшими: отдельный файл больше нескольких десятков КБ заслуживает сокращения или генерации.
Как уменьшить конфликты слияния в JSON-файлах?
JSON конфликтует легко: форматтер перестраивает весь файл сразу, поэтому два человека, каждый меняющий одну строку, конфликтуют по всему файлу.
Четыре тактики:
- Унифицируйте форматирование и добавьте его в
.editorconfig: согласованные отступы, окончания строк и завершающий перевод строки избегают посторонних различий; - Упорядочивайте ключи: договоритесь об алфавитном порядке, чтобы новое поле не сталкивалось с точкой вставки другого;
- Разделяйте файлы: разбейте один гигантский
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 |
| Null | Используйте null для отсутствия значения, опускайте неактуальные поля, опускание в PATCH означает «не менять» |
| Именование булевых | Префиксы is/has/can, без отрицаний, без 0/1 |
| Перечисления | Строки, строчные буквы с подчёркиваниями, добавить enum в Schema |
| Формат конфигурации | JSON для программной валидации, TOML или JSONC для ручного редактирования |
| Фикстуры | Один базовый вариант + переопределения минимальных различий, валидные по Schema |
| Конфликты слияния | Унифицировать форматирование, упорядочить ключи, разделить файлы |
| Совместимость версий | Добавлять, не менять, не удалять; помечать устаревшим перед удалением |