FAQ de colaboração em equipe: contratos, configuração e fixtures
Resumo: Estilo de nomeação, formatos de hora, semântica de null, formas de enum, formatos de configuração, organização de fixtures e conflitos de merge — doze trade-offs sobre os quais as equipes realmente discutem.

Resumo da ópera: resolva estas questões uma vez em uma convenção de equipe — um envelope {"data":…,"meta":…} com códigos de status HTTP, um único estilo de nomeação em todo o site, timestamps RFC 3339 com fuso horário e enums de string — depois evolua cada API nova contra o mesmo contrato apenas adicionando campos, nunca alterando ou excluindo.
Não há uma única resposta correta para essas perguntas, mas a equipe precisa ter uma única resposta — caso contrário toda API nova reinicia a mesma discussão.
1. Contratos de API
Que estrutura uma API deve retornar?
Três formatos comuns, cada um com um custo:
| Formato | Exemplo | Prós | Contras |
|---|---|---|---|
| Dados crus | [{...}, {...}] |
Simples | Não consegue carregar metadados |
| Envelope fixo | {"code":0,"message":"ok","data":{...}} |
Tratamento de erro uniforme | O código de negócio duplica o status HTTP |
| Envelope de recurso | {"data":{...},"meta":{...},"links":{...}} |
Extensível, semântica clara | Um nível extra de aninhamento |
Para APIs de negócio internas, prefira "envelope de recurso + códigos de status HTTP que expressam o resultado":
{
"data": [{ "id": "1", "name": "示例" }],
"meta": { "total": 128, "page": 2, "pageSize": 20 }
}
Para erros, use um código de status HTTP mais um corpo de erro estruturado em vez de aninhar outro code:
{
"error": {
"type": "validation_failed",
"message": "请求参数校验失败",
"details": [
{ "path": "/email", "reason": "格式不正确" }
]
}
}
Ponto-chave: os códigos de status HTTP já expressam sucesso ou falha, então um código de negócio é redundante. Quando os dois sistemas coexistem, inconsistências como "201 mas o code é 500" são quase inevitáveis.
Evite arrays crus. Com um array no nível superior você não consegue anexar paginação, totais ou cursores, e adicioná-los depois significa mudar todo chamador.
Nomes de campo devem usar snake_case ou camelCase?
Escolha um, aplique em todo o site e nunca os misture dentro de um mesmo documento JSON.
| Estilo | Ecossistema comum | Exemplo |
|---|---|---|
camelCase |
JavaScript / TypeScript / Java | userName, createdAt |
snake_case |
Python / Ruby / Go (em parte) / colunas de banco de dados | user_name, created_at |
kebab-case |
Raro; chaves com hífen são desconfortáveis de acessar | user-name |
Critérios:
- Quem consome este JSON: escolha camelCase quando o frontend domina, snake_case quando pipelines de dados e serviços Python dominam;
- Ele mapeia diretamente para colunas de banco de dados: snake_case economiza uma camada de conversão ao repassar estruturas de tabela diretamente;
- Continuidade dos sistemas existentes: mantenha o que a API existente usa; consistência vence correção.
Se você realmente precisa converter, faça na fronteira (gateway ou camada de requisição do frontend) em vez de espalhar chamadas toCamelCase pelo código de negócio.
Que formato os campos de hora devem usar?
Use strings RFC 3339 / ISO 8601 com um offset de fuso horário:
{
"createdAt": "2026-10-30T11:37:57+08:00",
"updatedAt": "2026-10-30T03:37:57Z"
}
Pontos-chave:
- Inclua o fuso horário:
2026-10-30 11:37:57sem fuso vai inevitavelmente quebrar em cenários entre fusos; - Use
Zou um offset de forma consistente: ambos são corretos, mas não os misture dentro de uma API; - Evite timestamps numéricos: se são segundos ou milissegundos, e se segundos bissextos estão incluídos, toda equipe precisa reinterpretar. Quando a compacidade for necessária, use um inteiro em milissegundos, mas fixe a unidade na documentação.
Prefira nomes de campo com sufixo At, como createdAt / updatedAt / deletedAt,
para que fique óbvio à primeira vista que o valor é um ponto no tempo e não uma duração. Para durações use timeoutMs, ttlSeconds — coloque a unidade no nome.
Nulos devem ser null ou um campo omitido?
Depende se "este campo é significativo":
| Semântica | Forma | Observações |
|---|---|---|
| O campo existe mas ainda não tem valor | "name": null |
Diz explicitamente "foi verificado e está vazio" |
| O campo é irrelevante para este recurso | Omitir | Por exemplo, um usuário comum não deve ter vipExpireAt |
| Atualização parcial (PATCH) | Omitir = não mudar | Só null significa "limpar" aqui |
O terceiro caso é o mais comumente negligenciado. Em uma requisição PATCH:
{ "nickname": null }
isso significa "limpar o apelido", enquanto omitir nickname do corpo da requisição significa "não tocar no apelido".
Se você trata os dois como "limpar", um usuário que muda o e-mail apagará o apelido.
Mais uma vez: não use undefined. Ele não é um valor JSON válido, e JSON.stringify descarta a chave por completo,
então o comportamento corresponde a "omitir" enquanto o código parece "um valor vazio foi passado explicitamente".
Como campos booleanos devem ser nomeados?
Use os prefixos is / has / can / should e evite negações.
{
"isActive": true,
"hasPassword": false,
"canEdit": true
}
Três lições:
- Sem negações:
isDisabled: falseforça o leitor a inverter uma vez;isEnabled: trueé mais direto. Dupla negação (isNotDisabled) é um desastre; - Não use números para booleanos:
"enabled": 1faz todo consumidor escrever=== 1uma vez; - Não use strings:
"enable": "true"é verdadeiro em JavaScript, eif (data.enable)também é verdadeiro para"false"; esses bugs são extremamente difíceis de rastrear.
Campos de enum devem usar números ou strings?
Use strings, a menos que haja uma restrição clara de armazenamento ou largura de banda.
{ "status": "active" }
Comparado com { "status": 1 }, strings vencem porque:
- Logs, capturas de pacotes e bancos de dados ficam diretamente legíveis sem consultar uma tabela de mapeamento;
- Adicionar um valor de enum não muda o significado dos valores existentes (inserir um valor no meio de um enum numérico desloca tudo);
- O frontend não precisa manter um mapeamento
1 → 'active'em sincronia.
O custo são alguns bytes extras, quase desprezíveis após a compressão.
Prática complementar: use minúsculas mais underscores para valores de enum de string (pending_review), e os restrinja com enum no Schema
para que valores inválidos sejam rejeitados na etapa de validação.
2. Configuração e fixtures
Configuração deve ser JSON, YAML ou TOML?
Escolha por finalidade, não por preferência:
| Formato | Bom para | Ruim para |
|---|---|---|
| JSON | Leitura e gravação programática, dados estritamente estruturados, validação por Schema | Edição manual frequente (comentários não são permitidos) |
| YAML | Configuração de implantação profundamente aninhada (K8s) | Indentação escrita à mão é propensa a erro; yes/no viram booleanos |
| TOML | Configuração plana de dependências e ferramentas (Cargo.toml, pyproject.toml) |
Legibilidade ruim para aninhamento profundo |
| JSONC | Configuração de editor que precisa de comentários (tsconfig.json) |
Cruzar fronteiras de sistema |
Um erro comum nas equipes é armazenar configuração de negócio em YAML — a conversão implícita de tipos do YAML (1.0 vira número,
NO vira booleano, 2026-10-30 vira data) produz resultados diferentes entre analisadores.
Se a configuração precisa ser estritamente validada por um programa, escolha JSON + Schema, e documente comentários por um campo _comment ou por docs separados.
Como fixtures de teste devem ser organizadas?
Organize por "uma base + diffs mínimos", não copiando um JSON enorme para cada caso de teste.
fixtures/
├─ users/
│ ├─ base.json # Objeto de usuário base
│ ├─ no-email.json # Apenas o e-mail está ausente
│ └─ invalid-age.json # Apenas a idade está fora da faixa
└─ fixtures.ts # Funções utilitárias para carregar e mesclar superficialmente
import baseUser from './users/base.json';
export function makeUser(overrides: Partial<User> = {}): User {
return { ...baseUser, ...overrides };
}
Assim um novo caso de teste só precisa escrever os campos diferentes, e mudar um campo base atualiza todos os casos automaticamente.
Três regras:
- As fixtures precisam passar na validação do Schema: a fixture base em si deve ser um dado válido;
- Não exporte dados reais de produção: números de telefone, documentos de identidade e endereços são todos informações pessoais;
- As fixtures pertencem ao controle de versão mas precisam permanecer pequenas: um único arquivo além de algumas dezenas de KB merece poda ou geração.
Como reduzo conflitos de merge em arquivos JSON?
JSON conflita facilmente: um formatador reflui o arquivo inteiro de uma vez, então duas pessoas mudando uma linha cada conflitam no arquivo todo.
Quatro táticas:
- Unifique a formatação e adicione-a ao
.editorconfig: indentação, fins de linha e nova linha final consistentes evitam diffs não relacionados; - Ordene as chaves: acorde uma ordenação alfabética para que um campo novo não colida com o ponto de inserção de outra pessoa;
- Divida arquivos: quebre um
config.jsongigante em vários arquivos por domínio, e a superfície de conflito encolhe naturalmente; - Evite commits de "reformatar tudo": separe a formatação das mudanças de negócio em dois commits para que a revisão permaneça legível.
Se a equipe usa arquivos de lock (pnpm-lock.yaml, package-lock.json), não os mescle à mão; regenere-os:
git checkout --theirs pnpm-lock.yaml
pnpm install
Como mantenho a compatibilidade de versão?
O princípio é "adicionar, não alterar, não remover".
| Mudança | Compatibilidade | Abordagem |
|---|---|---|
| Adicionar um campo opcional | Compatível | Adicione; consumidores ignoram campos desconhecidos |
| Adicionar um campo obrigatório | Incompatível | Suba a versão, ou forneça um padrão |
| Remover um campo | Incompatível | Marque como obsoleto primeiro, acompanhe os chamadores e depois remova |
| Mudar o tipo de um campo | Incompatível | Adicione um novo nome de campo e grave em duplicidade durante a transição |
| Mudar o conjunto de valores de enum | Depende | Adicionar valores é compatível; removê-los não é |
Mecanismos de suporte:
- Consumidores usam "análise leniente": ignoram campos desconhecidos e fornecem padrões para os ausentes;
- O servidor mantém campos antigos por um tempo e retorna um cabeçalho
Deprecationna resposta como dica; - No Schema, não marque campos novos como
required, e etiquete campos obsoletos comdeprecated: true.
Como valido contratos no CI?
Trate o Schema como parte do código e valide a cada commit.
# Valide dados de amostra contra o Schema (usando ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false
Rode três coisas no CI:
- Validade do Schema em si: se a declaração
$schemaé válida e se$refresolve; - Se as fixtures correspondem ao Schema: garante que os dados de teste não derivem;
- Se o contrato é compatível retroativamente: compare com a versão anterior e verifique campos removidos ou tipos alterados.
O terceiro item precisa de suporte de ferramentas (como openapi-diff ou json-schema-diff);
sem ele, ao menos compare mudanças nos arrays required e nos conjuntos de chaves de properties com um script.
3. Referência rápida
| Pergunta | Recomendação |
|---|---|
| Estrutura de resposta | {"data":..., "meta":...}, com códigos de status HTTP para erros |
| Estilo de nomeação | camelCase (frontend-first) ou snake_case (data-first), consistente em todo o site |
| Formato de hora | Strings RFC 3339 com fuso horário, nomes de campo terminando em At |
| Nulos | Use null para sem valor, omita campos irrelevantes, omissão em PATCH significa "não mudar" |
| Nomeação de booleanos | Prefixos is/has/can, sem negações, sem 0/1 |
| Enums | Strings, minúsculas com underscores, adicione enum no Schema |
| Formato de configuração | JSON para validação programática, TOML ou JSONC para edição manual |
| Fixtures | Uma base + sobrescritas de diff mínimo, válidas contra o Schema |
| Conflitos de merge | Unifique a formatação, ordene chaves, divida arquivos |
| Compatibilidade de versão | Adicione, não altere, não exclua; marque obsoleto antes de remover |