FAQ sulla collaborazione di squadra: contratti, configurazione e fixture
In sintesi: Stile dei nomi, formati delle date, semantica di null, forma degli enum, formati di configurazione, organizzazione dei fixture e conflitti di merge — dodici compromessi su cui le squadre discutono davvero.

In sintesi: stabilisci una volta per tutte nella convenzione di squadra — un envelope {"data":…,"meta":…} con codici di stato HTTP, un unico stile di denominazione in tutto il sito, timestamp RFC 3339 con fuso orario e enum come stringhe — poi fai evolvere ogni nuova API contro lo stesso contratto solo aggiungendo campi, senza mai modificarli o eliminarli.
Non esiste un'unica risposta corretta a queste domande, ma una squadra deve avere un'unica risposta — altrimenti ogni nuova API riavvia la stessa discussione.
1. Contratti API
Quale struttura dovrebbe restituire un'API?
Tre forme comuni, ciascuna con un costo:
| Forma | Esempio | Vantaggi | Svantaggi |
|---|---|---|---|
| Dati grezzi | [{...}, {...}] |
Semplice | Non può veicolare metadati |
| Envelope fisso | {"code":0,"message":"ok","data":{...}} |
Gestione uniforme degli errori | Il codice di business duplica lo stato HTTP |
| Envelope di risorsa | {"data":{...},"meta":{...},"links":{...}} |
Estensibile, semantica chiara | Un livello di annidamento in più |
Per le API di business interne, preferisci "envelope di risorsa + codici di stato HTTP che esprimono l'esito":
{
"data": [{ "id": "1", "name": "示例" }],
"meta": { "total": 128, "page": 2, "pageSize": 20 }
}
Per gli errori, usa un codice di stato HTTP più un corpo di errore strutturato invece di annidare un altro code:
{
"error": {
"type": "validation_failed",
"message": "请求参数校验失败",
"details": [
{ "path": "/email", "reason": "格式不正确" }
]
}
}
Punto chiave: i codici di stato HTTP esprimono già successo o insuccesso, quindi un codice di business è ridondante. Quando i due sistemi convivono, incoerenze come "201 ma il code è 500" sono quasi inevitabili.
Evita gli array nudi. Con un array al livello superiore non puoi allegare paginazione, totali o cursori, e aggiungerli in seguito significa modificare ogni chiamante.
I nomi dei campi dovrebbero usare snake_case o camelCase?
Scegline uno, applicalo in tutto il sito e non mescolarli mai all'interno di un singolo documento JSON.
| Stile | Ecosistema comune | Esempio |
|---|---|---|
camelCase |
JavaScript / TypeScript / Java | userName, createdAt |
snake_case |
Python / Ruby / Go (in parte) / colonne di database | user_name, created_at |
kebab-case |
Raro; le chiavi con trattino sono scomode da accedere | user-name |
Criteri:
- Chi consuma questo JSON: scegli camelCase quando domina il frontend, snake_case quando dominano pipeline di dati e servizi Python;
- Si mappa direttamente sulle colonne del database: snake_case risparmia uno strato di conversione quando si passano direttamente le strutture delle tabelle;
- Continuità dei sistemi esistenti: mantieni ciò che usa l'API esistente; la coerenza batte la correttezza.
Se devi davvero convertire, fallo al confine (gateway o livello delle richieste del frontend) invece di spargere chiamate toCamelCase nel codice di business.
Che formato dovrebbero usare i campi temporali?
Usa stringhe RFC 3339 / ISO 8601 con un offset di fuso orario:
{
"createdAt": "2026-10-30T11:37:57+08:00",
"updatedAt": "2026-10-30T03:37:57Z"
}
Punti chiave:
- Includi il fuso orario:
2026-10-30 11:37:57senza fuso orario si romperà inevitabilmente negli scenari tra fusi orari diversi; - Usa
Zo un offset in modo coerente: entrambi sono corretti, ma non mescolarli all'interno della stessa API; - Evita i timestamp numerici: che siano secondi o millisecondi, e che includano o meno i secondi intercalari, ogni squadra deve reinterpretarli. Quando serve compattezza, usa un intero in millisecondi, ma fissa l'unità nella documentazione.
Preferisci nomi di campo con suffisso At come createdAt / updatedAt / deletedAt,
così è evidente a colpo d'occhio che il valore è un istante nel tempo e non una durata. Per le durate usa timeoutMs, ttlSeconds — metti l'unità nel nome.
I null dovrebbero essere null o un campo omesso?
Dipende da se "questo campo ha un significato":
| Semantica | Forma | Note |
|---|---|---|
| Il campo esiste ma non ha ancora un valore | "name": null |
Dice esplicitamente "è stato controllato ed è vuoto" |
| Il campo non è pertinente a questa risorsa | Omesso | Per esempio, un utente normale non dovrebbe avere vipExpireAt |
| Aggiornamento parziale (PATCH) | Omesso = non modificare | Qui solo null significa "svuota" |
Il terzo caso è quello più spesso trascurato. In una richiesta PATCH:
{ "nickname": null }
questo significa "svuota il nickname", mentre lasciare nickname fuori dal corpo della richiesta significa "non toccare il nickname".
Se tratti entrambi come "svuota", un utente che cambia la propria email cancellerà il proprio nickname.
Ancora una volta: non usare undefined. Non è un valore JSON valido, e JSON.stringify elimina del tutto la chiave,
quindi il comportamento corrisponde a "omesso" mentre il codice sembra dire "è stato passato esplicitamente un valore vuoto".
Come dovrebbero essere denominati i campi booleani?
Usa i prefissi is / has / can / should ed evita le negazioni.
{
"isActive": true,
"hasPassword": false,
"canEdit": true
}
Tre lezioni:
- Niente negazioni:
isDisabled: falsecostringe il lettore a invertirlo una volta;isEnabled: trueè più diretto. La doppia negazione (isNotDisabled) è un disastro; - Non usare numeri per i booleani:
"enabled": 1fa sì che ogni consumatore scriva=== 1una volta; - Non usare stringhe:
"enable": "true"è truthy in JavaScript, eif (data.enable)è vero anche per"false"; bug di questo tipo sono estremamente difficili da rintracciare.
I campi enum dovrebbero usare numeri o stringhe?
Usa le stringhe, a meno che non vi sia un chiaro vincolo di archiviazione o larghezza di banda.
{ "status": "active" }
Rispetto a { "status": 1 }, le stringhe vincono perché:
- Log, catture di pacchetti e database sono direttamente leggibili senza consultare una tabella di mapping;
- Aggiungere un valore enum non cambia il significato dei valori esistenti (inserire un valore nel mezzo di un enum numerico sposta tutto);
- Il frontend non deve mantenere sincronizzata una mappatura
1 → 'active'.
Il costo sono qualche byte in più, quasi trascurabile dopo la compressione.
Pratica complementare: usa minuscole + underscore per i valori enum stringa (pending_review), e vincolali con enum nello Schema
così i valori invalidi vengono rifiutati in fase di validazione.
2. Configurazione e fixture
La configurazione dovrebbe essere JSON, YAML o TOML?
Scegli per scopo, non per preferenza:
| Formato | Adatto a | Inadatto a |
|---|---|---|
| JSON | Lettura e scrittura programmatica, dati strettamente strutturati, validazione con Schema | Modifica manuale frequente (i commenti non sono ammessi) |
| YAML | Configurazione di deployment profondamente annidata (K8s) | L'indentazione scritta a mano è soggetta a errori; yes/no diventano booleani |
| TOML | Configurazione piatta di dipendenze e strumenti (Cargo.toml, pyproject.toml) |
Leggibilità scarsa per annidamenti profondi |
| JSONC | Configurazione dell'editor che richiede commenti (tsconfig.json) |
Attraversare i confini di sistema |
Un errore comune nelle squadre è archiviare la configurazione di business in YAML — la conversione implicita dei tipi di YAML (1.0 diventa un numero,
NO diventa un booleano, 2026-10-30 diventa una data) produce risultati diversi tra parser.
Se la configurazione deve essere validata rigorosamente da un programma, scegli JSON + Schema, e documenta i commenti tramite un campo _comment o documentazione separata.
Come dovrebbero essere organizzati i fixture di test?
Organizza per "una base + diff minimi", non copiando un enorme JSON per ogni caso di test.
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 };
}
Così un nuovo caso di test deve scrivere solo i campi che differiscono, e modificare un campo della base aggiorna automaticamente ogni caso.
Tre regole:
- I fixture devono superare la validazione dello Schema: il fixture base stesso deve essere dati validi;
- Non esportare dati reali dalla produzione: numeri di telefono, numeri di documento e indirizzi sono tutti dati personali;
- I fixture appartengono al controllo di versione ma devono restare piccoli: un singolo file oltre qualche decina di KB merita di essere ridotto o generato.
Come posso ridurre i conflitti di merge nei file JSON?
JSON va facilmente in conflitto: un formattatore riformatta l'intero file in una volta, quindi due persone che modificano ciascuna una riga entrano in conflitto sull'intero file.
Quattro tattiche:
- Unifica la formattazione e aggiungila a
.editorconfig: indentazione, terminatori di riga e newline finale coerenti evitano diff non pertinenti; - Ordina le chiavi: concorda un ordine alfabetico così un nuovo campo non collide con il punto di inserimento di qualcun altro;
- Dividi i file: spezza un enorme
config.jsonin più file per dominio, e la superficie di conflitto si riduce naturalmente; - Evita i commit "riformatta tutto": separa la formattazione dalle modifiche di business in due commit così la revisione resta leggibile.
Se la squadra usa file di lock (pnpm-lock.yaml, package-lock.json), non fare il merge a mano; rigenerali:
git checkout --theirs pnpm-lock.yaml
pnpm install
Come posso mantenere la compatibilità delle versioni?
Il principio è "aggiungi, non modificare, non rimuovere".
| Modifica | Compatibilità | Approccio |
|---|---|---|
| Aggiungere un campo opzionale | Compatibile | Aggiungilo; i consumatori ignorano i campi sconosciuti |
| Aggiungere un campo obbligatorio | Incompatibile | Incrementa la versione, oppure fornisci un default |
| Rimuovere un campo | Incompatibile | Marchialo prima come deprecato, monitora i chiamanti, poi rimuovilo |
| Cambiare il tipo di un campo | Incompatibile | Aggiungi un nuovo nome di campo e scrivi in doppio durante la transizione |
| Cambiare l'insieme dei valori enum | Dipende | Aggiungere valori è compatibile; rimuoverli non lo è |
Meccanismi di supporto:
- I consumatori usano un "parsing indulgente": ignorano i campi sconosciuti e forniscono default per quelli mancanti;
- Il server mantiene i vecchi campi per un po' e restituisce un header
Deprecationnella risposta come suggerimento; - Nello Schema, non marcare i nuovi campi come
required, e contrassegna i campi deprecati condeprecated: true.
Come posso validare i contratti in CI?
Tratta lo Schema come parte del codice e validalo a ogni commit.
# Validate sample data against the Schema (using ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false
Esegui tre cose in CI:
- Validità dello Schema stesso: se la dichiarazione
$schemaè valida e se$refsi risolve; - Se i fixture corrispondono allo Schema: assicura che i dati di test non derivino;
- Se il contratto è retrocompatibile: confronta con la versione precedente e controlla campi rimossi o tipi modificati.
Il terzo elemento richiede supporto di strumenti (come openapi-diff o json-schema-diff);
senza di essi, almeno confronta con uno script i cambiamenti negli array required e negli insiemi di chiavi di properties.