Las API REST son uno de los tipos más comunes de servicios web disponibles, pero también son difíciles de diseñar. Permiten que varios clientes, incluidos navegadores, aplicaciones de escritorio, aplicaciones móviles y, básicamente, cualquier dispositivo con conexión a Internet, se comuniquen con un servidor.
Por lo tanto, es muy importante diseñar las API REST correctamente para no encontrarnos con problemas en el futuro.
Crear una API desde cero puede ser abrumador debido a la cantidad de cosas que deben estar en su lugar. Desde la seguridad básica hasta el uso de los métodos HTTP adecuados, pasando por la implementación de la autenticación, la decisión de qué solicitudes y respuestas se aceptan y devuelven, entre muchas otras.
En este post, estoy haciendo mi mejor esfuerzo para comprimir en 15 artículos algunas recomendaciones de gran alcance sobre lo que hace una buena API. Todos los consejos son independientes del lenguaje, por lo que pueden aplicarse a cualquier framework o tecnología.
Asegúrate de usar sustantivos en las rutas de los endpoints
Siempre debemos utilizar los sustantivos que representan la entidad que estamos recuperando o manipulando como el nombre de la ruta y siempre a favor de utilizar designaciones plurales. Evita usar verbos en las rutas de los endpoints porque nuestro método de petición HTTP ya tiene el verbo y realmente no añade ninguna información nueva.
La acción debe venir indicada por el método de petición HTTP que estemos realizando. Los métodos más comunes son GET, POST, PATCH, PUT y DELETE.
- GET recupera recursos.
- POST envía nuevos datos al servidor.
- PUT/PATCH actualiza los datos existentes.
- DELETE elimina datos.
Los verbos corresponden a operaciones CRUD.
Con estos principios en mente, deberíamos crear rutas como GET /books
para obtener una lista de libros y no GET /get-books
ni GET /book
.
Del mismo modo, POST /books
es para añadir un nuevo libro, PUT /books/:id
es para actualizar los datos completos del libro con un id dado, mientras que PATCH/books/:idactualiza
cambios parciales en el libro. Por último, DELETE /books/:id
sirve para eliminar un artículo existente con el identificador dado.
JSON como formato principal para enviar y recibir datos
Hasta hace unos años, la aceptación y la respuesta a las solicitudes de API se realizaban principalmente en XML. Pero hoy en día, JSON (JavaScript Object Notation) se ha convertido en gran medida en el formato "estándar" para enviar y recibir datos de la API en la mayoría de las aplicaciones. Así que nuestro segundo punto recomienda asegurarnos de que nuestros endpoints devuelven formato de datos JSON como respuesta y también al aceptar información a través de la carga útil de los mensajes HTTP.
Mientras que Form Data es bueno para enviar datos desde el cliente, especialmente si queremos enviar archivos, no es ideal para texto y números. No necesitamos Form Data para transferir esos ya que con la mayoría de frameworks podemos transferir JSON directamente del lado del cliente.
Cuando recibimos datos del cliente, tenemos que asegurarnos de que el cliente interpreta los datos JSON correctamente, y para ello el tipo Content-Type
en la cabecera de respuesta debe establecerse como application/json
al hacer la petición.
Vale la pena mencionar una vez más la excepción si estamos tratando de enviar y recibir archivos entre el cliente y el servidor. Para este caso en particular necesitamos manejar respuestas de archivos y enviar datos de formularios del cliente al servidor.
Utilizar un conjunto de códigos de estado HTTP predecibles
Siempre es una buena idea utilizar códigos de estado HTTP según sus definiciones para indicar el éxito o el fracaso de una solicitud. No utilices demasiados y utiliza los mismos códigos de estado para los mismos resultados en toda la API. Algunos ejemplos son
200
para éxito general.201
para una creación correcta.400
para solicitudes erróneas del cliente, como parámetros no válidos.401
para solicitudes no autorizadas.403
para permisos faltantes en los recursos.404
para los recursos que faltan429
para demasiadas peticiones5xx
para errores internos (deben evitarse en la medida de lo posible)
Puede haber más dependiendo de tu caso de uso, pero limitar la cantidad de códigos de estado ayuda al cliente a consumir una API más predecible.
Devolver mensajes estandarizados
Además del uso de códigos de estado HTTP que indican el resultado de la solicitud, utiliza siempre respuestas estandarizadas para puntos finales similares. Los consumidores siempre pueden esperar la misma estructura y actuar en consecuencia.
Esto se aplica tanto a los mensajes de éxito como a los de error. En el caso de la obtención de colecciones, aténgase a un formato concreto, ya sea que el cuerpo de la respuesta incluya una matriz de datos como ésta:
[
{
bookId: 1,
name: "The Republic"
},
{
bookId: 2,
name: "Animal Farm"
}
]
o un objeto combinado como éste:
{
"data": [
{
"bookId": 1,
"name": "The Republic"
},
{
"bookId": 2,
"name": "Animal Farm"
}
],
"totalDocs": 200,
"nextPageId": 3
}
El consejo es ser coherente independientemente del enfoque que elijas para esto. Debe implementarse el mismo comportamiento cuando se obtiene un objeto y también cuando se crean y actualizan recursos, para lo cual suele ser una buena idea devolver la última instancia del objeto.
// Response after successfully calling POST /books
{
"bookId": 3,
"name": "Brave New World"
}
Aunque no hace daño, es redundante incluir un mensaje genérico como "Libro creado con éxito", ya que está implícito en el código de estado HTTP.
Por último, pero no menos importante, los códigos de error son aún más importantes cuando se dispone de un formato de respuesta estándar.
Este mensaje debe incluir información que un cliente consumidor pueda utilizar para presentar los errores al usuario final como corresponde y no una alerta genérica del tipo "Algo ha ido mal", que deberíamos evitar en la medida de lo posible. He aquí un ejemplo:
{
"code": "book/not_found",
"message": "A book with the ID 6 could not be found"
}
De nuevo, no es necesario incluir el código de estado en el contenido de la respuesta, pero es útil definir un conjunto de códigos de error como book/not_found
para que el consumidor pueda asignarlos a diferentes cadenas y decidir su propio mensaje de error para el usuario.
En particular para los entornos de Desarrollo / Puesta en Escena puede parecer adecuado incluir también la pila de errores en la respuesta para ayudar a la depuración de errores. Pero, por favor, no los incluya en producción, ya que crearía un riesgo de seguridad al exponer información impredecible.
Utilizar la paginación, el filtrado y la ordenación al obtener colecciones de registros
Tan pronto como construyamos un endpoint que devuelva una lista de elementos, se debe aplicar la paginación. Las colecciones suelen crecer con el tiempo, por lo que es importante devolver siempre una cantidad limitada y controlada de elementos.
Es justo dejar que los consumidores de la API elijan cuántos objetos quieren obtener, pero siempre es buena idea predefinir un número y tener un máximo para ello. La razón principal es que devolver una gran cantidad de datos consume mucho tiempo y ancho de banda.
Para implementar la paginación, existen dos formas bien conocidas de hacerlo: skip/limit
o keyset
. La primera opción permite obtener los datos de una forma más sencilla, pero suele ser menos eficaz, ya que las bases de datos tendrán que escanear muchos documentos cuando obtengan los registros de la "línea inferior".
Por otro lado, y la que yo prefiero, la paginación keyset recibe un identificador/id
como referencia para "cortar" una colección o tabla con una condición sin escanear registros.
En la misma línea de pensamiento, las API deben proporcionar filtros y capacidades de ordenación que enriquezcan la forma de obtener los datos.
Para mejorar el rendimiento, los índices de las bases de datos forman parte de la solución para maximizar el rendimiento con los patrones de acceso que se aplican a través de estos filtros y opciones de ordenación.
Como parte del diseño de la API, estas propiedades de paginación, filtrado y ordenación se definen como parámetros de consulta en la URL. Por ejemplo, si queremos obtener los 10 primeros libros que pertenecen a una categoría "romance", nuestro endpoint tendría el siguiente aspecto:
GET /books?limit=10&category=romance
PATCH en lugar de PUT
Es muy poco probable que tengamos la necesidad de actualizar completamente un registro completo de una sola vez, normalmente hay datos sensibles o complejos que queremos mantener fuera de la manipulación del usuario.
Teniendo esto en cuenta, las peticiones PATCH
deberían utilizarse para realizar actualizaciones parciales de un recurso, mientras que PUT
reemplaza un recurso existente por completo.
Ambas deben usar el cuerpo de la petición para pasar la información a actualizar. Sólo los campos modificados en el caso de PATCH
y el objeto completo para las peticiones PUT
. Sin embargo, vale la pena mencionar que nada nos impide utilizar PUT
para actualizaciones parciales, no hay "restricciones de transferencia de red" que validen esto, es sólo una convención que es una buena idea seguir.
Proporcionar opciones de respuesta ampliadas
Los patrones de acceso son clave a la hora de crear los recursos disponibles de la API y qué datos se devuelven. Cuando un sistema crece, las propiedades de registro crecen también en ese proceso, pero no todas esas propiedades son siempre necesarias para que los clientes operen. Es en estas situaciones cuando resulta útil proporcionar la capacidad de devolver respuestas reducidas o completas para el mismo endpoint. 🤓
Si el consumidor sólo necesita algunos campos básicos, disponer de una respuesta simplificada ayuda a reducir el consumo de ancho de banda y, potencialmente, la complejidad de obtener otros campos calculados.
Una forma sencilla de abordar esta función es proporcionar un parámetro de consulta adicional para activar/desactivar
el suministro de la respuesta ampliada.
GET /books/:id
{
"bookId": 1,
"name": "The Republic"
}
GET /books/:id?extended=true
{
"bookId": 1,
"name": "The Republic"
"tags": ["philosophy", "history", "Greece"],
"author": {
"id": 1,
"name": "Plato"
}
}
Responsabilidad final
El Principio de Responsabilidad Única se centra en el concepto de mantener una función, método o clase, centrada en un comportamiento estrecho que hace bien.
Cuando pensamos en una API determinada, podemos decir que es una buena API si hace una cosa y nunca cambia. Esto ayuda a los consumidores a entender mejor nuestra API y a hacerla predecible, lo que facilita la integración general. Es preferible ampliar nuestra lista de endpoints disponibles para que sean más en total en lugar de construir endpoints muy complejos que intenten resolver muchas cosas al mismo tiempo.
Documentación precisa de la API
Los consumidores de su API deben ser capaces de entender cómo utilizar y qué esperar de los puntos finales disponibles. Esto sólo es posible con una documentación buena y detallada. Ten en cuenta los siguientes aspectos para proporcionar una API bien documentada.
- Endpoints disponibles describiendo el propósito de los mismos
- Permisos necesarios para ejecutar un endpoint
- Ejemplos de invocación y respuesta
- Mensajes de error esperables
La otra parte importante para que esto sea un éxito es tener la documentación siempre actualizada siguiendo los cambios y adiciones al sistema. La mejor manera de conseguirlo es hacer de la documentación de la API una pieza fundamental del desarrollo.
Dos herramientas muy conocidas en este sentido son Swagger y Postman, que están disponibles para la mayoría de los marcos de desarrollo de API.
Usar SSL para Seguridad y configurar CORS
La seguridad, otra propiedad fundamental que debe tener nuestra API. Configurar SSL instalando un certificado válido en el servidor garantizará una comunicación segura con los consumidores y evitará varios ataques potenciales.
CORS (Cross-origin resource sharing) es una característica de seguridad del navegador que restringe las peticiones HTTP cross-origin que se inician desde scripts que se ejecutan en el navegador. Si los recursos de tu API REST reciben solicitudes HTTP de origen cruzado no simples, debe activar la compatibilidad con CORS para que los consumidores funcionen en consecuencia.
El protocolo CORS requiere que el navegador envíe una solicitud de verificación previa al servidor y espere la aprobación (o una solicitud de credenciales) del servidor antes de enviar la solicitud real.
La solicitud de verificación previa aparece en la API como una solicitud HTTP que utiliza el método OPTIONS
(entre otras cabeceras). Por lo tanto, para admitir CORS, un recurso de API REST debe implementar un método OPTIONS
que pueda responder a la solicitud de verificación previa OPTIONS
con al menos las siguientes cabeceras de respuesta exigidas por el estándar Fetch
:
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Origin
Los valores que asignemos a estas claves dependerán de lo abierta y flexible que queramos que sea nuestra API. Podemos asignar métodos específicos y Orígenes conocidos o utilizar comodines para tener restricciones CORS abiertas.
Versionar la API
Como parte del proceso de evolución del desarrollo, los endpoints empiezan a cambiar y se reconstruyen. Pero debemos evitar en la medida de lo posible cambiar repentinamente los puntos finales para los consumidores. Es una buena idea pensar en la API como un recurso compatible con versiones anteriores en el que los puntos finales nuevos y actualizados deberían estar disponibles sin afectar a los estándares anteriores.
Aquí es donde el versionado de la API se vuelve útil, ya que los clientes deberían poder seleccionar a qué versión conectarse. Hay varias formas de declarar el versionado de la API:
1. Adding a new header "x-version=v2"
2. Having a query parameter "?apiVersion=2"
3. Making the version part of the URL: "/v2/books/:id"
Entrar en detalles sobre qué enfoque es más conveniente, cuándo hacer oficial una nueva versión y cuándo depreciar las versiones antiguas son sin duda preguntas interesantes que hacerse.
Datos en caché para mejorar el rendimiento
Para ayudar al rendimiento de nuestra API es beneficioso vigilar los datos que raramente cambian y a los que se accede con frecuencia. Para este tipo de datos podemos considerar el uso de una base de datos en memoria o caché que ahorre el acceso a la base de datos principal.
El principal problema de este enfoque es que los datos pueden quedar obsoletos, por lo que también habría que considerar un proceso para poner en marcha la última versión.
El uso de datos almacenados en caché será útil para que los consumidores carguen configuraciones y catálogos de información que no están destinados a cambiar muchas veces.
Cuando utilices el almacenamiento en caché, asegúrete de incluir la información Cache-Control en las cabeceras. Esto ayudará a los usuarios a utilizar eficazmente el sistema de almacenamiento en caché.
Utilizar fechas UTC estándar
No puedo pensar en la realidad de un sistema que no trabaje con fechas en algún momento. A nivel de datos, es importante ser coherente sobre cómo se muestran las fechas para las aplicaciones cliente.
La ISO 8601 es el formato estándar internacional para los datos relacionados con la fecha y la hora. Las fechas deben estar en formato "Z" o UTC, a partir del cual los clientes pueden decidir la zona horaria en caso de que sea necesario mostrar la fecha en cualquier circunstancia. He aquí un ejemplo de cómo deben ser las fechas:
{
"createdAt": "2022-03-08T19:15:08Z"
}
Un punto final de comprobación de la salud
Puede haber momentos difíciles en los que nuestra API esté caída y se tarde algún tiempo en ponerla en marcha. En estas circunstancias, a los clientes les gustará saber que los servicios no están disponibles para poder ser conscientes de la situación y actuar en consecuencia. Para conseguirlo, proporciona un punto final (como GET /health
) que determine si la API está en buen estado o no.
Este endpoint puede ser llamado por otras aplicaciones como balanceadores de carga. Incluso podemos ir un paso más allá e informar sobre periodos de mantenimiento o condiciones de salud en partes de la API.
Aceptar la autenticación mediante claves API
Permitir la autenticación mediante claves API ofrece a las aplicaciones de terceros la posibilidad de crear fácilmente una integración con nuestra API. Estas claves API deben pasarse utilizando una cabecera HTTP personalizada (como Api-Key o X-Api-Key). Las claves deben tener una fecha de caducidad, y debe ser posible revocarlas para que puedan ser invalidadas por razones de seguridad.
¡Gracias por leernos! 📖