APIs REST: recursos, métodos, respuestas y contratos
Una URL llamada /guardarEntregaDefinitiva puede devolver JSON y seguir dejando casi todas las decisiones importantes sin explicar. ¿Crea una entrega o sobrescribe otra? ¿Qué ocurre si la llamas dos veces? ¿El identificador corresponde al documento, al archivo o a su versión? Ponerle «REST» en la documentación no resuelve ninguna de esas preguntas. El nombre queda muy limpio; la ambigüedad continúa trabajando dentro.
Vamos a diseñar el contrato de una API ficticia de entregas y versiones. Veremos cómo identificar los recursos, expresar las operaciones y distinguir una respuesta correcta de un recurso ausente o una entrada inválida. Los mensajes son ejemplos de diseño, no capturas de un servicio disponible ni endpoints de Autodesk. Usaremos rutas relativas y cuerpos JSON para poder revisar lo que promete cada operación antes de escribir un servidor.
Un recurso necesita identidad antes de necesitar una URL bonita
Para este diseño, una entrega es una agrupación de información dentro de un proyecto. Tendrá un identificador estable, un nombre y una referencia a su última versión. Una versión identificará un estado concreto del contenido de esa entrega. Si mañana incorporamos otra revisión, la entrega podrá apuntar a una última versión diferente; la identidad de la anterior no debe desplazarse silenciosamente hacia el contenido nuevo.
Elegiremos /entregas/d-17 para una entrega y /entregas/d-17/versiones/v-04 para una de sus versiones. Los identificadores son opacos: el cliente los conserva y los utiliza, sin deducir de v-04 cuántos archivos contiene o si está aprobada. El nombre visible tampoco será la clave. Dos entregas de proyectos distintos pueden llamarse «Estructura» sin convertirse por ello en el mismo recurso. La API tendrá que comprobar la pertenencia al proyecto y los permisos; escribir el identificador en una ruta no concede acceso.
La relación entre entrega y versión evita una confusión habitual en los flujos BIM: abrir «la última» cuando el informe se calculó con una versión anterior. Si un proceso revisa v-04, su resultado debería conservar esa referencia. Un enlace a la entrega puede servir para navegar, pero no sustituye la identidad de la versión que fue evaluada. Agradezco mucho la comodidad de «última versión» hasta que se utiliza para explicar un resultado histórico.
Cuando el cliente solicite GET /entregas/d-17, proponemos una respuesta 200 con este cuerpo JSON:
Aquí enlaces es una convención propia del ejemplo, no el nombre obligatorio de una propiedad REST ni un formato estándar que acabemos de adoptar. Definiremos que sus rutas se resuelven respecto al origen de la API. El servidor ofrece relaciones que el cliente puede seguir; el cliente todavía necesita saber qué significa cada relación y qué operaciones están disponibles. Añadir tres enlaces no demuestra por sí solo que toda la arquitectura cumpla las restricciones de REST.
Una representación describe el recurso, no tiene que ser el archivo
El JSON anterior es una representación de la entrega: contiene información útil para un cliente, pero no es la entrega convertida mágicamente en texto ni incluye todo su contenido. Podríamos ofrecer metadatos de una versión y un enlace de descarga separado. La respuesta de metadatos tendría su estructura JSON; la descarga podría devolver un archivo con otro tipo de contenido. No obligaría al cliente a adivinar cuál de las dos cosas ha recibido mirando la extensión del enlace.
El contrato debe explicar qué campos son obligatorios, qué tipos aceptan y qué significa cada ausencia. Por ejemplo, podríamos decidir que una entrega recién creada tiene ultima_version_id igual a null, porque todavía no tiene versiones. Ese valor no significaría que se haya perdido una referencia existente. Si una propiedad puede faltar por permisos o por una modalidad de respuesta, habrá que documentarlo como otro caso. El frontend no debería rellenar esos huecos con la interpretación que menos código requiera.
También hay que declarar el alcance de las colecciones. GET /entregas podría devolver una página de resultados del usuario, no todas las entregas de la empresa. Si hay paginación, la respuesta debe indicar cómo continuar y qué criterios mantienen el recorrido estable. No basta con ver veinte objetos para concluir que existen veinte entregas. El transporte y el formato JSON se explican en HTTP y cliente-servidor; aquí estamos fijando el significado de aquello que viaja.
Para este ejemplo mantendremos separadas la representación de la entrega y la de una versión. La API podrá añadir más datos en el futuro, pero el cliente no debería depender de una posición accidental en una lista ni de un campo interno de almacenamiento que nunca se prometió. El contrato útil describe lo que otros pueden utilizar con estabilidad. Enseñar todas las columnas de la base de datos es más fácil, hasta que llega el día de cambiar una.
El método debe explicar qué puede pasar al repetir la operación
Usaremos GET para consultar entregas y versiones. Para crear una entrega, el cliente enviará POST /entregas con Content-Type: application/json. Proponemos que el nombre sea texto, tenga contenido tras retirar espacios de los extremos y no supere 100 caracteres después de esa limpieza. proyecto_id será un identificador de proyecto existente al que el usuario pueda añadir entregas. Estas son decisiones de nuestra API ficticia, no reglas universales del sector.
Una petición válida de creación tendría este cuerpo:
Si la entrega se crea en esa misma operación, responderemos 201 Created e incluiremos Location: /entregas/d-18. El cliente recibirá además la representación del recurso creado. En este escenario, d-18 es un identificador de ejemplo asignado por el servidor; no lo obtiene incrementando el que vio antes.
El código 201 de HTTP expresa creación, no una promesa de que cualquier procesamiento posterior haya terminado. Si nuestro sistema necesitase aceptar un trabajo y completarlo más tarde, tendríamos que diseñar ese otro recorrido y su estado consultable. No devolvería «entrega procesada» solo porque el servidor haya conseguido poner una tarea en una cola.
Repetir el POST anterior podría crear dos entregas, porque no hemos definido una protección contra duplicados ni un mecanismo de idempotencia. Idempotencia significa que repetir una operación tiene el mismo efecto previsto sobre el estado que realizarla una sola vez; no exige que cada respuesta sea idéntica. PUT tiene esa semántica en HTTP, pero no es un sustituto automático de cualquier creación con POST. Haría falta definir qué recurso conoce el cliente y qué representación está reemplazando. Elegir el verbo por la costumbre del framework es ceder una decisión de producto al autocompletado.
En una futura operación para renombrar una entrega también tendríamos que resolver modificaciones simultáneas. Si dos personas parten de nombres o estados distintos, aceptar siempre la última petición puede borrar una decisión sin advertirlo. La solución podría utilizar precondiciones y versiones de representación, pero necesita un contrato específico y pruebas. Este diseño introductorio todavía no implementa escrituras ni control de concurrencia.
Un recurso ausente y una entrada inválida necesitan respuestas diferentes
Supongamos que un cliente autorizado solicita GET /entregas/d-999 y esa entrega no existe. Elegiremos 404 Not Found y un cuerpo de error con un código estable para la aplicación:
El mensaje ayuda a una persona y puede traducirse. El código sirve al cliente para decidir cómo reaccionar sin comparar una frase completa. No incluiríamos rutas internas del servidor, consultas de base de datos ni credenciales en el error. Este caso supone que el solicitante ya puede consultar ese ámbito; cuando intervienen permisos, la política puede evitar revelar si un recurso ajeno existe. No vamos a deducir la identidad de una entrega restringida a partir de un estado.
Ahora cambia el problema: el cliente envía a POST /entregas un JSON bien formado, pero el nombre contiene solo espacios. El servidor entiende el documento y su tipo de contenido, aunque ese dato no cumple la regla de creación. Proponemos responder 422 Unprocessable Content con información sobre el campo:
El cliente puede señalar el nombre para que el usuario lo corrija. No tiene sentido repetir automáticamente la misma petición esperando que, con suficiente insistencia, los espacios adquieran contenido. El estado 422 permite expresar este rechazo del contenido entendido. Otras APIs eligen 400 para sus errores de validación; lo decisivo al consumirlas es seguir su contrato, no imponerles el que hemos diseñado aquí.
La sintaxis JSON rota, un tipo de contenido no admitido y un fallo interno requieren tratamiento propio. Para nuestra API documentaríamos 400 para sintaxis inválida y 415 para un tipo de contenido no soportado. También indicaríamos qué errores pueden llevar un cuerpo estructurado y qué debe hacer el cliente cuando un intermediario conteste con otro formato. Una respuesta correcta a nivel de transporte no convierte automáticamente su cuerpo en la estructura de error que esperabas.
REST exige más que repartir operaciones entre cuatro verbos
REST es un estilo arquitectónico con restricciones, descrito por Roy Fielding en su trabajo original. Incluye separación cliente-servidor, peticiones autosuficientes respecto al contexto de sesión necesario para atenderlas, condiciones de caché, una interfaz uniforme y una organización por capas; el código bajo demanda es una restricción opcional. No se resume en usar JSON ni en poner nombres en plural. La interfaz uniforme también contempla representaciones, mensajes que se describen y relaciones que guían la interacción.
La ausencia de estado de sesión en ese sentido no significa que el servidor no guarde proyectos, usuarios o entregas. Significa que no debería necesitar reconstruir la conversación previa de un cliente para entender la petición actual. La autenticación, los permisos y las reglas siguen siendo necesarios. Tampoco basta con cambiar cada verbo de una URL por un sustantivo para demostrar las propiedades de la arquitectura completa. El diseño anterior utiliza recursos y semántica HTTP, pero sus ejemplos aislados no acreditan por sí solos un sistema REST completo.
Para que alguien pueda implementar el cliente, convertiría estas decisiones en una descripción de rutas, parámetros, cuerpos, respuestas y condiciones. OpenAPI proporciona un formato para describir APIs HTTP de forma que puedan leerlo personas y herramientas. Puede ayudar a generar documentación y pruebas, aunque un documento correcto no demuestra que el servidor lo cumpla ni certifica que la arquitectura sea REST. El siguiente trabajo sería comprobar cada promesa contra la implementación.
Antes de programar esta API, ya podemos revisar tres recorridos concretos: consultar una entrega existente, solicitar una ausente y rechazar una creación cuyo nombre esté vacío. También sabemos qué referencia debe conservar un informe y qué ocurre si repetimos una creación. Son decisiones bastante más útiles que discutir el nombre del controlador mientras el comportamiento sigue sin decidirse. Si cambiarías algo del contrato, tráelo a los comentarios con una petición y una respuesta de ejemplo; así podremos discutir una consecuencia, no solo una preferencia estética.
Respuestas