Team Collaboration FAQ: Contracts, Config, and Fixtures
Bottom line: Naming style, time formats, null semantics, enum shapes, config formats, fixture organization, and merge conflicts — twelve trade-offs that teams genuinely argue about.

Bottom line: settle these once in a team convention — a {"data":…,"meta":…} envelope with HTTP status codes, one naming style site-wide, RFC 3339 timestamps with a time zone, and string enums — then evolve every new API against the same contract by adding fields only, never changing or deleting them.
There is no single correct answer to these questions, but a team must have a single answer — otherwise every new API restarts the same argument.
1. API contracts
What structure should an API return?
Three common shapes, each with a cost:
| Shape | Example | Pros | Cons |
|---|---|---|---|
| Raw data | [{...}, {...}] |
Simple | Cannot carry metadata |
| Fixed envelope | {"code":0,"message":"ok","data":{...}} |
Uniform error handling | Business code duplicates the HTTP status |
| Resource envelope | {"data":{...},"meta":{...},"links":{...}} |
Extensible, clear semantics | One extra level of nesting |
For internal business APIs, prefer "resource envelope + HTTP status codes that express the outcome":
{
"data": [{ "id": "1", "name": "示例" }],
"meta": { "total": 128, "page": 2, "pageSize": 20 }
}
For errors, use an HTTP status code plus a structured error body instead of nesting another code:
{
"error": {
"type": "validation_failed",
"message": "请求参数校验失败",
"details": [
{ "path": "/email", "reason": "格式不正确" }
]
}
}
Key point: HTTP status codes already express success or failure, so a business code is redundant. When the two systems coexist, inconsistencies such as "201 but code is 500" are almost inevitable.
Avoid bare arrays. With an array at the top level you cannot attach pagination, totals, or cursors, and adding them later means changing every caller.
Should field names use snake_case or camelCase?
Pick one, apply it site-wide, and never mix them within one JSON document.
| Style | Common ecosystem | Example |
|---|---|---|
camelCase |
JavaScript / TypeScript / Java | userName, createdAt |
snake_case |
Python / Ruby / Go (partly) / database columns | user_name, created_at |
kebab-case |
Rare; hyphenated keys are awkward to access | user-name |
Criteria:
- Who consumes this JSON: choose camelCase when the frontend dominates, snake_case when data pipelines and Python services dominate;
- Does it map directly to database columns: snake_case saves a conversion layer when passing table structures straight through;
- Continuity of existing systems: keep whatever the existing API uses; consistency beats correctness.
If you really must convert, do it at the boundary (gateway or frontend request layer) rather than scattering toCamelCase calls through business code.
What format should time fields use?
Use RFC 3339 / ISO 8601 strings with a time zone offset:
{
"createdAt": "2026-10-30T11:37:57+08:00",
"updatedAt": "2026-10-30T03:37:57Z"
}
Key points:
- Include the time zone:
2026-10-30 11:37:57without a zone will inevitably break in cross-time-zone scenarios; - Use
Zor an offset consistently: both are correct, but do not mix them within one API; - Avoid numeric timestamps: whether it is seconds or milliseconds, and whether leap seconds are included, every team has to reinterpret it. When compactness is required, use a millisecond integer, but pin the unit in the documentation.
Prefer field names with an At suffix such as createdAt / updatedAt / deletedAt,
so it is obvious at a glance that the value is a point in time rather than a duration. For durations use timeoutMs, ttlSeconds — put the unit in the name.
Should nulls be null or an omitted field?
It depends on whether "this field is meaningful":
| Semantics | Form | Notes |
|---|---|---|
| The field exists but has no value yet | "name": null |
Explicitly says "it was checked and it is empty" |
| The field is irrelevant to this resource | Omit | For example, a regular user should not have vipExpireAt |
| Partial update (PATCH) | Omit = do not change | Only null means "clear" here |
The third case is the most commonly overlooked. In a PATCH request:
{ "nickname": null }
this means "clear the nickname", whereas leaving nickname out of the request body means "do not touch the nickname".
If you treat both as "clear", a user changing their email will wipe their nickname.
Once more: do not use undefined. It is not a valid JSON value, and JSON.stringify drops the key entirely,
so the behavior matches "omit" while the code looks like "an empty value was passed explicitly".
How should boolean fields be named?
Use the is / has / can / should prefixes and avoid negations.
{
"isActive": true,
"hasPassword": false,
"canEdit": true
}
Three lessons:
- No negations:
isDisabled: falseforces the reader to invert it once;isEnabled: trueis more direct. Double negation (isNotDisabled) is a disaster; - Do not use numbers for booleans:
"enabled": 1makes every consumer write=== 1once; - Do not use strings:
"enable": "true"is truthy in JavaScript, andif (data.enable)is also true for"false"; such bugs are extremely hard to trace.
Should enum fields use numbers or strings?
Use strings unless there is a clear storage or bandwidth constraint.
{ "status": "active" }
Compared with { "status": 1 }, strings win because:
- Logs, packet captures, and databases are directly readable without looking up a mapping table;
- Adding an enum value does not change the meaning of existing values (inserting a value in the middle of a numeric enum shifts everything);
- The frontend does not need to maintain a
1 → 'active'mapping in sync.
The cost is a few extra bytes, which is almost negligible after compression.
Companion practice: use lowercase + underscores for string enum values (pending_review), and constrain them with enum in the Schema
so invalid values are rejected at the validation stage.
2. Configuration and fixtures
Should config be JSON, YAML, or TOML?
Choose by purpose, not by preference:
| Format | Good for | Bad for |
|---|---|---|
| JSON | Programmatic reading and writing, strictly structured data, Schema validation | Frequent manual editing (comments are not allowed) |
| YAML | Deeply nested deployment config (K8s) | Hand-written indentation is error-prone; yes/no turn into booleans |
| TOML | Flat dependency and tool config (Cargo.toml, pyproject.toml) |
Poor readability for deep nesting |
| JSONC | Editor config that needs comments (tsconfig.json) |
Crossing system boundaries |
A common mistake in teams is storing business config in YAML — YAML's implicit type conversion (1.0 becomes a number,
NO becomes a boolean, 2026-10-30 becomes a date) produces different results across parsers.
If the config must be strictly validated by a program, choose JSON + Schema, and document comments through a _comment field or separate docs.
How should test fixtures be organized?
Organize by "one baseline + minimal diffs", not by copying a huge JSON for every test case.
fixtures/
├─ users/
│ ├─ base.json # Baseline user object
│ ├─ no-email.json # Only email is missing
│ └─ invalid-age.json # Only age is out of range
└─ fixtures.ts # Utility functions for loading and shallow merging
import baseUser from './users/base.json';
export function makeUser(overrides: Partial<User> = {}): User {
return { ...baseUser, ...overrides };
}
That way a new test case only needs to write the differing fields, and changing a baseline field updates every case automatically.
Three rules:
- Fixtures must pass Schema validation: the baseline fixture itself should be valid data;
- Do not export real data from production: phone numbers, ID numbers, and addresses are all personal information;
- Fixtures belong in version control but must stay small: a single file beyond a few tens of KB deserves pruning or generation.
How do I reduce merge conflicts in JSON files?
JSON conflicts easily: a formatter reflows the whole file at once, so two people each changing one line conflict on the entire file.
Four tactics:
- Unify formatting and add it to
.editorconfig: consistent indentation, line endings, and trailing newline avoid unrelated diffs; - Sort keys: agree on alphabetical ordering so a new field does not collide with someone else's insertion point;
- Split files: break one giant
config.jsoninto several files by domain, and the conflict surface shrinks naturally; - Avoid "reformat everything" commits: separate formatting from business changes into two commits so the review stays readable.
If the team uses lock files (pnpm-lock.yaml, package-lock.json), do not merge them by hand; regenerate them instead:
git checkout --theirs pnpm-lock.yaml
pnpm install
How do I maintain version compatibility?
The principle is "add, do not change, do not remove".
| Change | Compatibility | Approach |
|---|---|---|
| Add an optional field | Compatible | Add it; consumers ignore unknown fields |
| Add a required field | Incompatible | Bump the version, or provide a default |
| Remove a field | Incompatible | Mark it deprecated first, watch the callers, then remove |
| Change a field type | Incompatible | Add a new field name and dual-write during the transition |
| Change the enum value set | Depends | Adding values is compatible; removing them is not |
Supporting mechanisms:
- Consumers use "lenient parsing": ignore unknown fields and supply defaults for missing ones;
- The server keeps old fields for a while and returns a
Deprecationheader in the response as a hint; - In the Schema, do not mark new fields as
required, and tag deprecated fields withdeprecated: true.
How do I validate contracts in CI?
Treat the Schema as part of the code and validate it on every commit.
# Validate sample data against the Schema (using ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false
Run three things in CI:
- Validity of the Schema itself: whether the
$schemadeclaration is valid and whether$refresolves; - Whether the fixtures match the Schema: ensures test data does not drift;
- Whether the contract is backward compatible: compare against the previous version and check for removed fields or changed types.
The third item needs tooling support (such as openapi-diff or json-schema-diff);
without it, at least compare changes in the required arrays and the properties key sets with a script.
3. Quick reference
| Question | Recommendation |
|---|---|
| Response structure | {"data":..., "meta":...}, with HTTP status codes for errors |
| Naming style | camelCase (frontend-first) or snake_case (data-first), consistent site-wide |
| Time format | RFC 3339 strings with a time zone, field names ending in At |
| Nulls | Use null for no value, omit irrelevant fields, omission in PATCH means "do not change" |
| Boolean naming | is/has/can prefixes, no negations, no 0/1 |
| Enums | Strings, lowercase with underscores, add enum in the Schema |
| Config format | JSON for programmatic validation, TOML or JSONC for manual editing |
| Fixtures | One baseline + minimal-diff overrides, valid against the Schema |
| Merge conflicts | Unify formatting, sort keys, split files |
| Version compatibility | Add, do not change, do not delete; mark deprecated before removing |