FAQ de trabajo en equipo: contratos, configuración y fixtures
En resumen: Estilo de nomenclatura, formatos de hora, semántica de null, forma de los enums, formatos de configuración, organización de fixtures y conflictos de fusión: doce contrapartidas sobre las que los equipos realmente discuten.

En resumen: zanja estas cuestiones de una vez en una convención de equipo —un envoltorio {"data":…,"meta":…} con códigos de estado HTTP, un único estilo de nomenclatura en todo el sitio, marcas de tiempo RFC 3339 con zona horaria y enums de cadenas— y luego evoluciona cada API nueva contra el mismo contrato solo añadiendo campos, nunca cambiándolos ni eliminándolos.
No hay una única respuesta correcta a estas preguntas, pero un equipo debe tener una única respuesta; de lo contrario, cada API nueva reinicia la misma discusión.
1. Contratos de API
¿Qué estructura debería devolver una API?
Tres formas habituales, cada una con un coste:
| Forma | Ejemplo | Pros | Contras |
|---|---|---|---|
| Datos en crudo | [{...}, {...}] |
Simple | No puede llevar metadatos |
| Sobre fijo | {"code":0,"message":"ok","data":{...}} |
Tratamiento de errores uniforme | El código de negocio duplica el estado HTTP |
| Sobre de recurso | {"data":{...},"meta":{...},"links":{...}} |
Extensible, semántica clara | Un nivel extra de anidamiento |
Para las APIs de negocio internas, prefiere «sobre de recurso + códigos de estado HTTP que expresen el resultado»:
{
"data": [{ "id": "1", "name": "示例" }],
"meta": { "total": 128, "page": 2, "pageSize": 20 }
}
Para los errores, usa un código de estado HTTP más un cuerpo de error estructurado en lugar de anidar otro code:
{
"error": {
"type": "validation_failed",
"message": "请求参数校验失败",
"details": [
{ "path": "/email", "reason": "格式不正确" }
]
}
}
Punto clave: los códigos de estado HTTP ya expresan éxito o fracaso, así que un código de negocio es redundante. Cuando ambos sistemas conviven, las inconsistencias del tipo «201 pero el código es 500» son casi inevitables.
Evita los arrays desnudos. Con un array en el nivel superior no puedes adjuntar paginación, totales ni cursores, y añadirlos después implica cambiar a todos los llamadores.
¿Los nombres de campo deberían usar snake_case o camelCase?
Elige uno, aplícalo en todo el sitio y no los mezcles nunca dentro de un mismo documento JSON.
| Estilo | Ecosistema habitual | Ejemplo |
|---|---|---|
camelCase |
JavaScript / TypeScript / Java | userName, createdAt |
snake_case |
Python / Ruby / Go (en parte) / columnas de base de datos | user_name, created_at |
kebab-case |
Poco común; las claves con guion son incómodas de acceder | user-name |
Criterios:
- Quién consume este JSON: elige camelCase cuando domine el frontend, snake_case cuando dominen los pipelines de datos y los servicios en Python;
- ¿Se mapea directamente a columnas de base de datos?: snake_case ahorra una capa de conversión si pasas las estructuras de tabla tal cual;
- Continuidad de los sistemas existentes: mantén lo que use la API existente; la consistencia pesa más que la corrección.
Si de verdad tienes que convertir, hazlo en la frontera (gateway o capa de peticiones del frontend) en lugar de dispersar llamadas toCamelCase por el código de negocio.
¿Qué formato deberían usar los campos de hora?
Usa cadenas RFC 3339 / ISO 8601 con un desfase de zona horaria:
{
"createdAt": "2026-10-30T11:37:57+08:00",
"updatedAt": "2026-10-30T03:37:57Z"
}
Puntos clave:
- Incluye la zona horaria:
2026-10-30 11:37:57sin zona acabará rompiendo en escenarios de varias zonas horarias; - Usa
Zo un desfase de forma consistente: ambos son correctos, pero no los mezcles dentro de una misma API; - Evita las marcas de tiempo numéricas: si son segundos o milisegundos, y si incluyen segundos intercalares, cada equipo tiene que reinterpretarlo. Cuando se requiera compacidad, usa un entero de milisegundos, pero fija la unidad en la documentación.
Prefiere nombres de campo con sufijo At, como createdAt / updatedAt / deletedAt,
para que se vea a simple vista que el valor es un punto en el tiempo y no una duración. Para duraciones usa timeoutMs, ttlSeconds: pon la unidad en el nombre.
¿Los nulos deberían ser null o un campo omitido?
Depende de si «este campo tiene sentido»:
| Semántica | Forma | Notas |
|---|---|---|
| El campo existe pero aún no tiene valor | "name": null |
Dice explícitamente «se comprobó y está vacío» |
| El campo es irrelevante para este recurso | Omitir | Por ejemplo, un usuario normal no debería tener vipExpireAt |
| Actualización parcial (PATCH) | Omitir = no cambiar | Solo null significa «borrar» aquí |
El tercer caso es el que más se pasa por alto. En una petición PATCH:
{ "nickname": null }
esto significa «borra el apodo», mientras que dejar nickname fuera del cuerpo de la petición significa «no toques el apodo».
Si tratas ambos como «borrar», un usuario que cambie su correo borrará su apodo.
Una vez más: no uses undefined. No es un valor JSON válido, y JSON.stringify elimina la clave por completo,
así que el comportamiento coincide con «omitir» mientras el código parece decir «se pasó un valor vacío explícitamente».
¿Cómo deberían nombrarse los campos booleanos?
Usa los prefijos is / has / can / should y evita las negaciones.
{
"isActive": true,
"hasPassword": false,
"canEdit": true
}
Tres lecciones:
- Sin negaciones:
isDisabled: falseobliga al lector a invertirlo una vez;isEnabled: truees más directo. La doble negación (isNotDisabled) es un desastre; - No uses números para los booleanos:
"enabled": 1obliga a cada consumidor a escribir=== 1una vez; - No uses cadenas:
"enable": "true"es truthy en JavaScript, yif (data.enable)también es verdadero para"false"; ese tipo de errores cuesta muchísimo rastrearlos.
¿Los campos enum deberían usar números o cadenas?
Usa cadenas a menos que haya una restricción clara de almacenamiento o ancho de banda.
{ "status": "active" }
Comparado con { "status": 1 }, las cadenas ganan porque:
- Los logs, las capturas de paquetes y las bases de datos son directamente legibles sin consultar una tabla de mapeo;
- Añadir un valor al enum no cambia el significado de los valores existentes (insertar un valor en medio de un enum numérico desplaza todo);
- El frontend no necesita mantener sincronizado un mapeo
1 → 'active'.
El coste son unos pocos bytes extra, casi despreciables tras la compresión.
Práctica complementaria: usa minúsculas + guiones bajos para los valores de enum de cadena (pending_review), y restríngelos con enum en el Schema
para que los valores inválidos se rechacen en la fase de validación.
2. Configuración y fixtures
¿La configuración debería ser JSON, YAML o TOML?
Elige por propósito, no por preferencia:
| Formato | Bueno para | Malo para |
|---|---|---|
| JSON | Lectura y escritura programática, datos estrictamente estructurados, validación con Schema | Edición manual frecuente (no se admiten comentarios) |
| YAML | Configuración de despliegue muy anidada (K8s) | La indentación escrita a mano es propensa a errores; yes/no se convierten en booleanos |
| TOML | Configuración plana de dependencias y herramientas (Cargo.toml, pyproject.toml) |
Mala legibilidad con anidamiento profundo |
| JSONC | Configuración de editor que necesita comentarios (tsconfig.json) |
Cruzar fronteras entre sistemas |
Un error habitual en los equipos es guardar configuración de negocio en YAML: la conversión implícita de tipos de YAML (1.0 se convierte en número,
NO se convierte en booleano, 2026-10-30 se convierte en fecha) produce resultados distintos entre analizadores.
Si la configuración debe validarla estrictamente un programa, elige JSON + Schema, y documenta los comentarios mediante un campo _comment o documentación aparte.
¿Cómo deberían organizarse los fixtures de prueba?
Organízalos por «una base + diferencias mínimas», no copiando un JSON enorme para cada caso de prueba.
fixtures/
├─ users/
│ ├─ base.json # Objeto de usuario base
│ ├─ no-email.json # Solo falta email
│ └─ invalid-age.json # Solo age está fuera de rango
└─ fixtures.ts # Funciones de utilidad para cargar y fusionar superficialmente
import baseUser from './users/base.json';
export function makeUser(overrides: Partial<User> = {}): User {
return { ...baseUser, ...overrides };
}
Así, un caso de prueba nuevo solo necesita escribir los campos que difieren, y cambiar un campo de la base actualiza todos los casos automáticamente.
Tres reglas:
- Los fixtures deben pasar la validación del Schema: el propio fixture base debería ser dato válido;
- No exportes datos reales de producción: teléfonos, números de identificación y direcciones son todos información personal;
- Los fixtures van al control de versiones pero deben seguir siendo pequeños: un solo archivo de más de unas decenas de KB merece poda o generación.
¿Cómo reduzco los conflictos de fusión en archivos JSON?
JSON entra en conflicto con facilidad: un formateador reordena todo el archivo de golpe, así que dos personas que cambian una línea cada una entran en conflicto en el archivo completo.
Cuatro tácticas:
- Unificar el formato y añadirlo a
.editorconfig: indentación, finales de línea y salto de línea final consistentes evitan diffs irrelevantes; - Ordenar las claves: acuerda el orden alfabético para que un campo nuevo no choque con el punto de inserción de otro;
- Dividir archivos: parte un
config.jsongigante en varios archivos por dominio, y la superficie de conflicto se reduce de forma natural; - Evitar commits de «reformatear todo»: separa el formateo de los cambios de negocio en dos commits para que la revisión siga siendo legible.
Si el equipo usa archivos de bloqueo (pnpm-lock.yaml, package-lock.json), no los fusiones a mano; regenéralos:
git checkout --theirs pnpm-lock.yaml
pnpm install
¿Cómo mantengo la compatibilidad de versiones?
El principio es «añadir, no cambiar, no eliminar».
| Cambio | Compatibilidad | Enfoque |
|---|---|---|
| Añadir un campo opcional | Compatible | Añádelo; los consumidores ignoran los campos desconocidos |
| Añadir un campo obligatorio | Incompatible | Sube la versión, o proporciona un valor por defecto |
| Eliminar un campo | Incompatible | Márcalo primero como obsoleto, vigila los llamadores y luego elimínalo |
| Cambiar el tipo de un campo | Incompatible | Añade un nombre de campo nuevo y escribe en ambos durante la transición |
| Cambiar el conjunto de valores del enum | Depende | Añadir valores es compatible; eliminarlos no |
Mecanismos de apoyo:
- Los consumidores usan «análisis tolerante»: ignoran los campos desconocidos y aportan valores por defecto para los que faltan;
- El servidor conserva los campos antiguos durante un tiempo y devuelve una cabecera
Deprecationen la respuesta como aviso; - En el Schema, no marques los campos nuevos como
required, y etiqueta los obsoletos condeprecated: true.
¿Cómo valido los contratos en CI?
Trata el Schema como parte del código y valídalo en cada commit.
# Validar datos de ejemplo contra el Schema (usando ajv-cli)
npx ajv-cli validate -s schemas/user.json -d "fixtures/users/*.json" --strict=false
Ejecuta tres cosas en CI:
- Validez del propio Schema: si la declaración
$schemaes válida y si$refse resuelve; - Si los fixtures coinciden con el Schema: asegura que los datos de prueba no se desvían;
- Si el contrato es compatible hacia atrás: compáralo con la versión anterior y busca campos eliminados o tipos cambiados.
El tercer punto necesita soporte de herramientas (como openapi-diff o json-schema-diff);
sin él, al menos compara con un script los cambios en los arrays required y en los conjuntos de claves de properties.
3. Referencia rápida
| Pregunta | Recomendación |
|---|---|
| Estructura de respuesta | {"data":..., "meta":...}, con códigos de estado HTTP para los errores |
| Estilo de nomenclatura | camelCase (frontend primero) o snake_case (datos primero), consistente en todo el sitio |
| Formato de hora | Cadenas RFC 3339 con zona horaria, nombres de campo terminados en At |
| Nulos | Usa null para «sin valor», omite los campos irrelevantes, la omisión en PATCH significa «no cambiar» |
| Nomenclatura de booleanos | Prefijos is/has/can, sin negaciones, sin 0/1 |
| Enums | Cadenas, minúsculas con guiones bajos, añade enum en el Schema |
| Formato de configuración | JSON para validación programática, TOML o JSONC para edición manual |
| Fixtures | Una base + anulaciones de diferencia mínima, válidas contra el Schema |
| Conflictos de fusión | Unificar formato, ordenar claves, dividir archivos |
| Compatibilidad de versiones | Añadir, no cambiar, no borrar; marcar como obsoleto antes de eliminar |