Una API es un contrato: una promesa sobre qué solicitudes aceptará un servicio y qué formas de respuestas devolverá, hecha a los llamadores que no pueden ver —y no deberían necesitar ver— nada sobre la implementación detrás de ella. Diseñar una API bien significa diseñar esa promesa deliberadamente, antes del código, en lugar de dejar que surja con la forma que un manejador de rutas produjo.
Una API es un contrato estable entre un servicio y sus llamadores, y cada decisión de diseño —URL, verbos, códigos de estado, formas de error— existe para hacer que ese contrato sea predecible y seguro para depender de él.
Por qué es importante: Los llamadores escriben código basándose en las formas de tus respuestas; un cambio que parece trivial internamente (renombrar un campo, cambiar un código de estado) es un cambio disruptivo externamente si no formaba parte del contrato documentado.
Conceptos clave:recurso, idempotencia, semántica de código de estado, estabilidad del contrato, cambio disruptivo, versionado.
Cuándo usarlo: Al diseñar nuevos endpoints, decidir si un cambio es seguro de implementar sin un aumento de versión, elegir entre rutas de estilo REST y RPC, y revisar si el manejo de errores de una API es consistente.
Limitaciones / Compensaciones: Un contrato estricto y bien documentado es más lento de cambiar que uno no documentado; cada mejora debe sopesarse con el costo de romper algo para alguien que ya depende de la forma actual.
Temas relacionados: Modelado de recursos REST, semántica de códigos de estado HTTP, estándares de respuesta de error, versionado de API, especificación OpenAPI.
La palabra "diseño" en diseño de API está haciendo un trabajo real: un recurso es el sustantivo que expone una API —un pedido, un cliente, una factura— y los métodos HTTP (GET, POST, PATCH, DELETE) son los verbos que se le aplican.
Esa forma orientada a recursos no es una elección estética; es lo que permite a un llamador que nunca ha leído tu código fuente adivinar correctamente que DELETE /orders/:id elimina un pedido, sin una línea de documentación.
Una alternativa de estilo RPC —POST /deleteOrder— también funciona, pero obliga a que cada acción se aprenda individualmente, porque la URL ya no lleva ningún significado por sí misma.
Una analogía útil: una API bien diseñada es una máquina expendedora, no una conversación.
Un llamador no necesita explicar lo que quiere en prosa o adivinar el estado interno: presiona un botón bien etiquetado (una URL de recurso más un método) y obtiene un resultado predecible y documentado cada vez, independientemente de lo que esté sucediendo dentro de la máquina.
GET /v1/orders/:id # obtener un pedidoPOST /v1/orders # crear un pedidoPATCH /v1/orders/:id # actualizar parcialmente un pedidoDELETE /v1/orders/:id # eliminar un pedido
La idempotencia es la propiedad de que llamar a una operación dos veces tiene el mismo efecto que llamarla una vez —se espera que PUT y DELETE sean idempotentes según la propia semántica de HTTP, mientras que POST generalmente no lo es, por lo que los endpoints de pago a menudo agregan un encabezado Idempotency-Key explícito para obtener esa garantía donde el método HTTP por sí solo no la proporciona.
Una vez que una API se lanza y un llamador escribe código contra ella, la forma de la respuesta se convierte en una infraestructura de soporte que el propietario de la API ya no controla por completo: una aplicación móvil en el bolsillo de un usuario, o la integración de un socio, sigue llamando a la forma antigua hasta que alguien actualiza ese código, lo cual el equipo de la API no puede forzar ni siempre ver que sucede.
Esta es la tensión central en el diseño de API: internamente, la refactorización es barata porque cada llamador es un compañero de trabajo que puede actualizar en la misma solicitud de extracción; externamente, una "refactorización" de una forma de respuesta es un cambio disruptivo que se lanza en su propio cronograma, invisible para el equipo que lo hizo hasta que llegan los tickets de soporte.
Los códigos de estado tienen un significado independiente del cuerpo de la respuesta, lo que los hace útiles para la ramificación programática antes de que un cliente incluso analice JSON.
// El código de estado es lo primero en lo que un cliente se ramifica, antes del análisis del cuerpoif (response.status === 404) { // el llamador puede reaccionar a "no encontrado" sin leer response.body en absoluto} else if (response.status >= 500) { // es seguro reintentar; 4xx generalmente no lo es}
Esa distinción entre 4xx y 5xx es un mecanismo, no una convención: los clientes HTTP, los proxies y las bibliotecas de reintento tratan los dos rangos de manera diferente por defecto (los 5xx a menudo se reintentan automáticamente; los 4xx generalmente no), por lo que devolver 500 para un error de validación le dice a la infraestructura intermedia que reintente una solicitud que fallará idénticamente cada vez.
Un envoltorio consistente —envolver las cargas útiles de éxito en { data: ... } y los errores en { error: ... }— existe por una razón relacionada: permite a un cliente escribir una única pieza de lógica de manejo de respuestas para cada endpoint en la API, en lugar de un caso especial por ruta. Sin esa consistencia, cada nuevo endpoint es un pequeño proyecto de integración para cada consumidor, porque nada de los diecinueve endpoints anteriores predice la forma del vigésimo.
La estabilidad del contrato tiene un costo genuino, y comprender ese costo es lo que separa el diseño deliberado de API de la precaución excesiva o la imprudencia. Un campo que en realidad no es utilizado por ningún llamador puede eliminarse libremente; un campo del que una integración depende silenciosamente no puede, incluso si parece no utilizado desde dentro de la base de código, por lo que las API de producción instrumentan cada vez más el uso de la respuesta (qué campos leen realmente los clientes) antes de eliminar cualquier cosa.
El versionado existe para permitir que un contrato evolucione sin romper a los llamadores existentes: una nueva versión principal (/v2/orders) puede cambiar las formas de respuesta libremente, mientras que /v1 sigue sirviendo exactamente lo que siempre ha hecho hasta que se deprecia formalmente en un cronograma publicado. La alternativa —mutar /v1 en su lugar— intercambia un esquema de URL limpio por un contrato impredecible, lo cual es un peor intercambio para cualquier API con llamadores fuera del control directo del equipo.
Verboso para acciones puras que no son realmente CRUD
API públicas y B2B, dominios con mucho CRUD
Estilo RPC (/doThing)
Fácil de agregar acciones únicas
No hay vocabulario compartido entre endpoints; más difícil de documentar genéricamente
Endpoints internos con muchas acciones, operaciones de estilo comando
GraphQL
Los clientes obtienen exactamente los campos que necesitan; un solo endpoint
El almacenamiento en caché y la limitación de velocidad son más difíciles; configuración del lado del servidor más compleja
Interfaces de usuario con muchos datos y muchas formas de cliente desde un solo backend
El manejo de errores merece la misma disciplina contractual que las respuestas de éxito, y a menudo recibe menos en la práctica. Un campo code estable y legible por máquina (ORDER_NOT_FOUND) permite a los clientes ramificarse por significado; una cadena message legible por humanos no lo hace, porque esa cadena puede cambiar con una edición de texto y romper silenciosamente cualquier cliente que estuviera haciendo coincidir patrones con su texto exacto. Estándares de respuesta de error cubre la forma de RFC 9457 Problem Details que esta pila estandariza, pero el principio subyacente se aplica independientemente del formato: cualquier cosa en la que se espere que un cliente se ramifique programáticamente debe ser tan estable como el propio contrato.
Las herramientas modernas han trasladado parte de esta disciplina de la convención a la aplicación. Una especificación OpenAPI, generada a partir de código o escrita a mano y validada en CI, convierte "el contrato es lo que dicen los documentos" en "el contrato es lo que una máquina puede verificar una respuesta" —detectando la desviación entre la documentación y la implementación antes de que un llamador note la discrepancia.
"Una API bien diseñada y un endpoint funcional son lo mismo." Un endpoint puede funcionar hoy y aún estar mal diseñado si su forma es inconsistente con el resto de la API o filtra detalles de implementación de los que un llamador no debería depender.
"Cambiar una implementación interna nunca afecta a la API." No lo hace, siempre y cuando la forma de la respuesta y los códigos de estado permanezcan idénticos; en el momento en que cualquiera de ellos cambia, es un cambio externo, a nivel de contrato, sin importar cuán pequeña haya sido la diferencia interna.
"REST significa solo CRUD, así que cualquier otra cosa necesita RPC." Las acciones no CRUD aún pueden modelarse como recursos: POST /orders/:id/cancel trata "cancelar" como una acción sobre un recurso en lugar de abandonar por completo el diseño orientado a recursos.
"El código de estado 200 con un error en el cuerpo es un atajo inofensivo." Anula cada pieza de infraestructura que se ramifica por código de estado (reintentos, almacenamiento en caché, monitoreo), lo que obliga a cada llamador a analizar el cuerpo solo para saber si una solicitud tuvo éxito.
"El versionado es opcional si el equipo es cuidadoso con la compatibilidad con versiones anteriores." Incluso los equipos cuidadosos eventualmente necesitan un cambio genuinamente disruptivo; el versionado es lo que hace que ese cambio sea soportable para los llamadores existentes en lugar de un día de cambio de bandera coordinado.
¿Qué hace que una API sea un "contrato" en lugar de solo un detalle de implementación?
Los llamadores externos al equipo —aplicaciones móviles, integraciones de socios, otros servicios— escriben código basándose en las formas de respuesta y los códigos de estado exactos que devuelve una API, y no pueden ver ni influir en cómo se produjo esa respuesta internamente.
¿Por qué es importante el diseño de URL orientado a recursos sobre los endpoints de estilo RPC?
Le da a cada endpoint un vocabulario compartido y predecible: un llamador que entiende GET /orders/:id puede predecir correctamente lo que hace GET /customers/:id, sin documentación, porque el patrón se repite.
¿Cómo cambian los códigos de estado el comportamiento del cliente, mecánicamente?
Los clientes HTTP, los proxies y las bibliotecas de reintento se ramifican en el rango del código de estado antes incluso de analizar el cuerpo de la respuesta; muchos reintentan automáticamente las respuestas 5xx y no reintentan las 4xx, por lo que el código que devuelves determina el comportamiento real de la infraestructura, no solo la semántica.
¿Cómo ayuda realmente un envoltorio de respuesta consistente a un cliente?
Permite a un cliente escribir una pieza de código compartida para verificar data versus error en cada endpoint, en lugar de escribir lógica de análisis personalizada por ruta porque la forma de cada endpoint es ligeramente diferente.
¿Cuándo es seguro cambiar una respuesta de API sin un aumento de versión?
Generalmente solo al agregar un nuevo campo opcional que ningún cliente existente lee todavía; eliminar un campo, renombrarlo o cambiar el tipo o significado de un campo es un cambio disruptivo, sin importar cuán pequeño parezca en la diferencia.
¿Por qué los clientes no deberían ramificarse en el texto del `message` de error?
El texto del mensaje está destinado a humanos y puede cambiar con una edición de texto en cualquier momento; un campo code estable y legible por máquina es la parte del contrato destinada a ser dependiente programáticamente.
¿Es REST siempre la opción correcta sobre GraphQL?
No, REST se adapta bien a escenarios con mucho CRUD, cacheables y con muchas herramientas, mientras que GraphQL se adapta a interfaces de usuario con muchos datos que necesitan muchas combinaciones de campos diferentes de un solo backend; la elección depende de la diversidad de la forma del llamador y las necesidades de almacenamiento en caché, no de que uno sea universalmente mejor.
¿Cuál es el costo real de una API mal diseñada, más allá de la estética?
Cada inconsistencia (una forma de envoltorio diferente, un código de estado impredecible) se convierte en trabajo de integración que cada llamador tiene que rehacer por endpoint, y cada "pequeño" cambio disruptivo se convierte en un incidente de soporte para quien dependía de la forma antigua.
¿Por qué la idempotencia es importante para las solicitudes `POST` específicamente?
POST no es idempotente según la propia semántica de HTTP, por lo que una solicitud reintentada (por una red inestable, por ejemplo) puede crear un recurso duplicado a menos que la API agregue un mecanismo explícito, como un encabezado Idempotency-Key, para hacer que los reintentos sean seguros.
¿Cómo cambia OpenAPI la forma en que se aplica un contrato?
Convierte el contrato de una descripción en un documento a un esquema verificable por máquina: una respuesta validada por la especificación coincide con lo prometido o falla una verificación, detectando automáticamente la desviación entre la documentación y el comportamiento real.
¿Por qué existe el versionado si un equipo es disciplinado con la compatibilidad con versiones anteriores?
Incluso los equipos disciplinados eventualmente necesitan un cambio que genuinamente no puede mantener la compatibilidad con versiones anteriores; el versionado les da a los llamadores existentes un objetivo estable (/v1) para seguir usando mientras los nuevos llamadores adoptan la nueva forma (/v2), en lugar de forzar a todos a un cambio disruptivo a la vez.