Según pasa el tiempo y vas haciendo servicios que tienen que escalarse e integrarse con otros servicios, te vas dando cuenta de lo necesario que es un buen diseño o por lo menos de un diseño mínimamente bien pensando y ejecutado que cumpla una serie de requisitos mínimos. Ésta es la idea que pretendo transmitiros, pero antes os ubico un poco.
En la entrada de Linkero – Creación de APIs RESTful en Python os presenté el framework Linkero que permitía montar APIs RESTful, y se presentaba junto con un ejemplo a forma de iniciación en las mismas. En este entrada voy presentar una serie de recomendaciones, que pueden considerarse buenas prácticas, y que debéis tener en cuenta para asegurar un buen diseño de vuestras APIs RESTful.
El concepto subyacente de una API RESTful es la de dividir la estructura de la API en recursos lógicos, montados sobre una URL, que nos permitan acceder a información o datos concretos usando métodos HTTP como POST, GET, PUT y DELETE para operar con los recursos. Estos métodos son equiparables a las operaciones CRUD (Create, Read, Update, Delete) de las bases de datos. Aclarado esto, empecemos con las recomendaciones.
Usar nombres pero no verbos
Por cuestiones de mejor comprensión y legibilidad de la semántica de la API, usa sólo nombres para toda su estructura.
Recurso | POST create | GET read | PUT update | DELETE |
---|---|---|---|---|
/cars | Crea un nuevo coche | Devuelve la lista de coches | Actualización en bloque de coches | Borrar todos los coches |
/cars/711 | Método no permitido (405) | Devuelve un coche específico | Actualiza un coche específico | Borra un coche específico |
No usar verbos como:
/getAllCars /createNewCar /deleteAllRedCar
La propia semántica que compone la URL, junto con el método HTTP usado (POST, GET, PUT, DELETE), permite prescindir de verbos como "create", "get", "update" y "delete".
Métodos GET y los parámetros de consulta no deben alterar el estado
Usa los métodos POST, PUT o DELETE para cambiar los estados de los recursos, en vez de hacerlo con el método GET o un petición con parámetros de consulta. Para algo tienes 3 métodos que te permiten hacer eso 😉 .
Esto es lo que no se debería hacer:
GET /users/711?activate GET /users/711/activate
Es mucho mejor hacer esto:
PUT /users/711/activate
o esto:
POST /users/711/activate
Cuál usar dependerá del diseño de la base de datos o la lógica con la que se estructura el almacenamiento de la misma.
Usar nombres en plural
No mezcles nombres en singular con nombres en plural. Hazlo simple y mantén los nombres en plural.
En vez de usar:
/car /user /product /setting
Usa el plural:
/cars /users /products /settings
El uso del plural te permitirá posteriormente hacer operaciones del tipo:
/cars/<id> /users/<id> /products/<id> /settings/<name>
Usar subrecursos para establecer relaciones
Si un recurso está relacionado con otro, usa subrecursos montados sobre la estructura de la URL.
GET /cars/711/drivers/ Devuelve una lista de conductores para el coche 711 GET /cars/711/drivers/4 Devuelve el conductor #4 para el coche 711
Usar cabeceras HTTP para la serialización de formatos
Tanto la parte cliente como la del servidor, necesitan saber en qué formato se están pasando los datos para poder comunicarse. Lo más sencillo es especificarlo en la cebecera HTTP.
Content-Type: Define el formato de la petición.
Accept: Define la lista de formatos aceptados en la respuesta.
Usar HATEOAS
El uso de HATEOAS (Hypermedia as the Engine of Application State) es un diseño que nos permite incluir el principio de hipervínculos de manera similar a la navegación web, lo que nos permite una mejor navegación por la API.
En este ejemplo la respuesta en formato JSON nos devuelve junto con la información del equipo con id 5000, una serie de referencias a recursos relacionados, como pueden ser el estadio del equipo o sus jugadores. Normalmente se suele agrupar todas las referencias dentro de una propiedad o atributo llamada "links".
{
"links": [{
"rel": "self",
"href": "http://localhost:8080/soccer/api/teams/5000"
}, {
"rel": "stadium",
"href": "http://localhost:8080/soccer/api/teams/5000/stadium"
}, {
"rel": "players",
"href": "http://localhost:8080/soccer/api/teams/5000/players"
}],
"teamId": 5000,
"name": "Real Madrid C.F.",
"foundationYear": 1902,
"rankingPosition": 1
}
Este tipo de diseño está pensado para la longevidad del software y la evolución independiente, ya que frecuentemente en la arquitectura de sistemas y servicios, es a corto plazo lo que más suele fallar.
Proveer filtrado, ordenación, selección de campos y paginación para colecciones
Filtrado
Utilizar un parámetro de consulta único para todos los campos o un lenguaje de consulta formalizado para filtrar.
GET /cars?color=red Devulve una lista de coches rojos GET /cars?seats<=2 Devuelve una lista de coches con un máximo de 2 plazas
Ordenación
Permitir ordenación ascendente o descendente sobre varios campos
GET /cars?sort=-manufactorer,+model
En el ejemplo se devuelve una lista de coches ordenada con los fabricantes de manera descendiente y los modelos de manera ascendente.
Selección de campos
Permitir la selección de unos pocos campos de un recurso si no se precisan todos. Dar al consumidor de la API la posibilidad de qué campos quiere que se devuelvan, reduce el volumen de tráfico en la comunicación y aumenta la velocidad de uso de la API.
GET /cars?fields=manufacturer,model,color
En el ejemplo se pide la lista de coches, pero pidiendo sólo los campos de "fabricante", "modelo" y "color".
Paginación
Uso de "offset" para establecer la posición de partida de una colección y "limit" para establecer el número de elementos de la colección a devolver desde el offset. Es un sistema flexible para el que consume la API y bastante extendido entre los sistemas de bases de datos.
GET /cars?offset=10&limit=5
En el ejemplo se devuelve una lista de coches correspondiente a los coches comprendidos de la posición 10 a la 15 de la colección.
Para devolver al consumidor de la API el número total de entradas se puede usar la cabecera HTTP "X-Total-Count" y para completar la información proporciona, por ejemplo, devolver los enlaces a la página previa y siguiente, entre otros, usando la cabecera HTTP "Link".
Link: <http://localhost/sample/api/v1/cars?offset=15&limit=5>; rel="next", <http://localhost/sample/api/v1/cars?offset=5&limit=5>; rel="prev", <http://localhost/sample/api/v1/cars?offset=50&limit=3>; rel="last", <http://localhost/sample/api/v1/cars?offset=0&limit=5>; rel="first",
Como alternativa es posible usar HATEOAS, como ya he comentado anteriormente.
Versionar la API
Es muy recomendable, tirando a obligatorio 😉 , el poner versión de API y no liberar APIs sin versión. Usa un simple número para especificar la versión y no uses un versionado tipo semver del tipo 1.5.
Si se usa la URL para marcar la versión, pon una "v" precediendo el número de versión.
/blog/api/v1
Es la forma más sencilla y eficaz de asegurar la compatibilidad en el versionado de la API. Especificarla como parámetro o en la petición y/o en la respuesta, conlleva el aumento del tráfico y el coste de computación, al tener que existir lógica para discriminar las distintas versiones soportadas sobre una misma URL.
Manejar errores con código de estado HTTP
Es difícil trabajar con una API que ignora el manejo de errores, por no decir imposible 😉 . La devolución de un código HTTP 500 para cualquier tipo de error o acompañado de una traza de error del código del servidor no es muy útil, además de que puede ponernos en peligro ante una vulnerabilidad que un atacante quisiera explotar.
Usar códigos de estado HTTP
El estándar HTTP proporciona más de 70 códigos de estado para describir los valores de retorno. En general no los utilizaremos todos, pero se debe utilizar por lo menos un mñinimo de 10, que suelen ser los más comunes:
Código | Significado | Explicación |
---|---|---|
200 | OK | Todo está funcionado |
201 | OK | Nuevo recurso ha sido creado |
204 | OK | El recurso ha sido borrado satisfactoriamente |
304 | No modificado | El cliente puede usar los datos cacheados |
400 | Bad Request | La petición es inválida o no puede ser servida. El error exacto debería ser explicado en el payload de respuesta |
401 | Unauthorized | La petición requiere de la autenticación del usuario |
403 | Forbidden | El servidor entiende la petición pero la rechaza o el acceso no está permitido |
404 | Not found | No hay un recurso tras la URI |
422 | Unprocessable Entity | Debe utilizarse si el servidor no puede procesar la entidad. Por ejemplo, faltan campos obligatorios en el payload. |
500 | Internal Server Error | Los desarrolladores de APIs deben evitar este error. Si se produce un error global, el stracktrace se debe registrar y no devolverlo como respuesta. |
Usar el payload para los errores
Todas las excepciones deben ser asignadas en un payload de error. A continuación se muestra un ejemplo de cómo podría verse un payload de error en formato JSON:
{
"errors": [
{
"userMessage": "Sorry, the requested resource does not exist",
"internalMessage": "No car found in the database",
"code": 34,
"more info": "http://dev.mascandobits.es/blog/api/v1/errors/12345"
}
]
}
Permitir la sustitución del método HTTP
Algunos proxys sólo admiten métodos POST y GET. Para que una API RESTful funcione con estas limitaciones, la API necesita una forma de sustituir el método HTTP.
Una opción es utilizar la cebecera HTTP "X-HTTP-Method-Override" para sobrescribir el método POST y así personalizar la petición POST para que cumpla, por ejemplo, una operación DELETE.
X-HTTP-Method-Override: DELETE
Espero que esta concisa guía os permita realizar APIs RESTful con un mejor diseño, o mejorar aquellas que ya tengáis desarrolladas, eso sí generando una nueva versión de la API 😉 .
Hola, muuuy bueno todo, me aclaró algunas dudas, pero aún tengo una muy importante y es con el tema de la Authentication, la doc's que he leído me me advierten de riesgos si no se usan bajo el protocolo ssl, pero cuéntame algo, que más debería prever aún usando ssl. ¡GRACIAS!
El SSL no hace realmente el servicio más seguro, sino privado. Puede repercutir en seguridad si usas por ejemplo una "autenticación" madando user y password, o una más recomendable de tipo token. En este caso es más seguro porque el canal queda protegido (es privado), asegurando que los únicos participantes en las comunicaciones es el servicio legítimo y el usuario. Son los únicos que pueden ver la información transferida, quedando fuera del alcance de ojos indiscreto.
Algo importante para evitar problemas de seguridad en tu servicio RestFul es evitar responder con información de la traza de errores que puedas devolver tu servicio ante un error inesperado (evitando dar información sobre el servicio y posibles vulnerabilidades para ataques). Eso ya lo comento en el post, pero también es importante que aunque nuestro servicio cumpla lo anterior, asegurar la "performance" del servicio evitando que se pueda hacer un ataque DDoS (Distributed Denial of Service). Para ello es necesario limitar o controlar el número de peticiones, ya que si no se hace, es susceptible de ser saturado el servicio.
Por último estar al tanto de vulnerabilidades en el lenguaje o framework que uses para aplicar parches o correcciones de seguridad o rendimiento. Si es desarrollo propio, esto es difícil. La única forma de garantizarlo es por escrutinio público liberando el código, y esperando que exista una colaboración proactiva y positiva.
No sé si este tipo de cosas es de las que pedías una ampliación. Por contarte puedo contarte muchas cosas más, pero si me concretas más, creo que podré resolver mejor tus dudas e inquietudes.
Muchas gracias estimado por tomarse el tiempo de explicar esto. Es un gran aporte. Genial
Gracias a ti por tomarte la molestia de escribir para decir que te ha gustado. Gratifica mucho el sentimiento de utilidad.
excelente artículo. muchas gracias por compartir tus conocimientos.
¡Gracias a ti por tus palabras! 😉
Me gusta este post. Gracias por el aporte. En uno de los comentarios vi que dejar un buen texto sobre seguridad. Tenes algun lugar donde puedar ver mas de esto? de como manejar la seguridad de mi API REST. Ademas del tema del token tambien como evitar que me bombardee con request y se me explote el server.