Buenas Prácticas para el Buen Diseño de una API RESTful

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.

RecursoPOST
create
GET
read
PUT
update
DELETE
/carsCrea un nuevo cocheDevuelve la lista de cochesActualización en bloque de cochesBorrar todos los coches
/cars/711Método no permitido (405)Devuelve un coche específicoActualiza un coche específicoBorra 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ódigoSignificadoExplicació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 😉 .

 





20114 Visitas Totales 4 Visitas para Hoy

8 response on “Buenas Prácticas para el Buen Diseño de una API RESTful

  1. 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.

  2. 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.