Mejores prácticas para el diseño de la API REST

Mejores prácticas para el diseño de la API REST

Las APIs REST son uno de los tipos más comunes de servicios web disponibles hoy en día. Permiten que varios clientes, incluidas las aplicaciones de navegador, se comuniquen con un servidor a través de la API REST. Por lo tanto, es muy importante diseñar las APIs REST adecuadamente para no encontrarnos con problemas en el futuro. Tenemos que tener en cuenta la seguridad, el rendimiento y la facilidad de uso para los consumidores de la API.


De lo contrario, creamos problemas a los clientes que utilizan nuestras APIs, lo que no es agradable y hace que la gente deje de utilizarlas. Si no seguimos las convenciones comúnmente aceptadas, entonces confundimos a los mantenedores de la API y a los clientes que las utilizan, ya que es diferente de lo que todos esperan.


En este artículo, veremos cómo diseñar las APIs REST para que sean fáciles de entender para cualquiera que las consuma, a prueba de futuro, y seguras y rápidas, ya que sirven datos a clientes que pueden ser confidenciales.


Índice de contenido

1. ¿Qué es una API REST?


2. Aceptar y responder con JSON


3. Utilizar nombres en lugar de verbos en las rutas de los puntos finales


4. Colecciones de nombres con sustantivos plurales


5. Recursos de anidamiento para objetos jerárquicos


6. Manejar los errores con elegancia y devolver los códigos de error estándar


7. Permitir el filtrado, la clasificación y la paginación


8. Mantener buenas prácticas de seguridad


9. Datos en caché para mejorar el rendimiento


10. Versionar nuestras APIs


11. Conclusión


¿Qué es una API REST?

Una API REST es una interfaz de programación de aplicaciones que se ajusta a restricciones arquitectónicas específicas, como la comunicación sin estado y los datos almacenados en caché. No es un protocolo ni un estándar. Aunque se puede acceder a las API REST a través de varios protocolos de comunicación, lo más habitual es que se llamen a través de HTTPS, por lo que las directrices que se indican a continuación se aplican a los puntos finales de las API REST que se llamarán a través de Internet.


NOTA:


En el caso de las APIs REST llamadas a través de Internet, es conveniente seguir las mejores prácticas de autenticación de las APIs REST.


Aceptar y responder con JSON

Las APIs REST deben aceptar JSON como carga útil de las solicitudes y también enviar las respuestas a JSON. JSON es el estándar para la transferencia de datos. Casi todas las tecnologías en red pueden utilizarlo: JavaScript tiene métodos incorporados para codificar y decodificar JSON, ya sea a través de la API Fetch o de otro cliente HTTP. Las tecnologías del lado del servidor tienen bibliotecas que pueden decodificar JSON sin hacer mucho trabajo.


Hay otras formas de transferir datos. XML no es ampliamente soportado por los frameworks sin transformar los datos nosotros mismos a algo que pueda ser usado, y eso es usualmente JSON. No podemos manipular estos datos tan fácilmente en el lado del cliente, especialmente en los navegadores. Acaba siendo un montón de trabajo extra sólo para hacer una transferencia de datos normal.


Los datos de formulario son buenos para enviar datos, especialmente si queremos enviar archivos. Pero para el texto y los números, no necesitamos datos de formulario para transferirlos ya que -con la mayoría de los frameworks- podemos transferir JSON simplemente obteniendo los datos de el directamente en el lado del cliente. Es, con mucho, lo más sencillo de hacer.


Para asegurarnos de que cuando nuestra aplicación de la API REST responda con JSON los clientes lo interpreten como tal, debemos establecer Content-Type en la cabecera de respuesta a application/json después de realizar la solicitud. Muchos frameworks de aplicaciones del lado del servidor establecen la cabecera de respuesta automáticamente. Algunos clientes HTTP miran la cabecera de respuesta Content-Type y analizan los datos de acuerdo con ese formato.


La única excepción es si intentamos enviar y recibir archivos entre el cliente y el servidor. Entonces se deben manejar las respuestas de los archivos y enviar los datos del formulario del cliente al servidor. También se debe asegurar de que los endpoints devuelven JSON como respuesta. Muchos frameworks del lado del servidor tienen esto como una característica incorporada.


Veamos un ejemplo de API que acepta cargas útiles JSON. Este ejemplo utilizará el framework de back-end Express para Node.js. Podemos utilizar el middleware body-parser para analizar el cuerpo de la solicitud JSON, y luego podemos llamar al método res.json con el objeto que queremos devolver como respuesta JSON de la siguiente manera:


bodyParser.json() analiza la cadena del cuerpo de la solicitud JSON en un objeto JavaScript y lo asigna al objeto req.body.


Establezca la cabecera Content-Type en la respuesta a application/json; charset=utf-8 sin ningún cambio. El método anterior se aplica a la mayoría de los demás frameworks de back-end.


Utilizar nombres en lugar de verbos en las rutas de los puntos finales

No deberíamos usar verbos en las rutas de nuestros endpoints. En su lugar, debemos utilizar los sustantivos que representan la entidad que el punto final que estamos recuperando o manipulando como el nombre de la ruta. Esto se debe a que nuestro método de solicitud HTTP ya tiene el verbo. Tener verbos en las rutas de nuestro punto final de la API no es útil y lo hace innecesariamente largo ya que no transmite ninguna información nueva. Los verbos elegidos podrían variar según el capricho del desarrollador. Por ejemplo, a algunos les gusta 'get' y a otros 'retrieve', así que es mejor dejar que el verbo HTTP GET nos diga lo que hace el endpoint.


La acción debe estar indicada por el método de petición HTTP que estemos realizando. Los métodos más comunes son GET, POST, PUT y DELETE.


  • GET recupera recursos.
  • POST envía nuevos datos al servidor.
  • PUT actualiza los datos existentes.
  • DELETE elimina los datos.

Los verbos corresponden a operaciones CRUD.


Teniendo en cuenta los dos principios que hemos comentado anteriormente, deberíamos crear rutas como GET /articles/ para obtener artículos de noticias. Igualmente, POST /articles/ es para añadir un nuevo artículo, PUT /articles/:id es para actualizar el artículo con el id dado. DELETE /articles/:id es para eliminar un artículo existente con el ID dado.


/articles representa un recurso de la API REST. Por ejemplo, podemos utilizar Express para añadir los siguientes endpoints para manipular artículos de la siguiente manera:


En el código anterior, hemos definido los puntos finales para manipular los artículos. Como podemos ver, los nombres de las rutas no tienen ningún verbo. Todo lo que tenemos son sustantivos. Los verbos están en los verbos HTTP. Los puntos finales POST, PUT y DELETE toman JSON como cuerpo de la petición, y todos devuelven JSON como respuesta, incluido el punto final GET.


Utilizar la anidación lógica en los puntos finales

Cuando se diseñan los puntos finales, tiene sentido agrupar aquellos que contienen información asociada. Es decir, si un objeto puede contener otro objeto, debe diseñar el endpoint para que lo refleje. Esta es una buena práctica, independientemente de que los datos estén estructurados así en su base de datos. De hecho, puede ser aconsejable evitar reflejar la estructura de tu base de datos en tus endpoints para evitar dar a los atacantes información innecesaria.


Por ejemplo, si queremos que un endpoint obtenga los comentarios de un artículo de noticias, debemos añadir la ruta /comments al final de la ruta /articles. Podemos hacerlo con el siguiente código en Express:


En el código anterior, podemos utilizar el método GET en la ruta '/articles/:articleId/comments'. Obtenemos los comments del artículo identificado por articleId y los devolvemos en la respuesta. Añadimos 'comments' después del segmento de la ruta '/articles/:articleId' para indicar que es un recurso hijo de /articles.


Esto tiene sentido ya que los comments son los objetos hijos de los articles De lo contrario, es confuso para el usuario ya que esta estructura es generalmente aceptada para acceder a los objetos hijos. El mismo principio se aplica también a los puntos finales POST, PUT y DELETE. Todos ellos pueden utilizar el mismo tipo de estructura de anidamiento para los nombres de ruta.


Sin embargo, el anidamiento puede ir demasiado lejos. A partir del segundo o tercer nivel, los puntos finales anidados pueden resultar poco manejables. Considere, en cambio, devolver la URL a esos recursos, especialmente si esos datos no están necesariamente contenidos en el objeto de nivel superior. 


Por ejemplo, supongamos que quiere devolver el autor de determinados comentarios. Podrías utilizar /articles/:articleId/comments/:commentId/author. Pero eso se te va de las manos. En su lugar, devuelve el URI de ese usuario en particular dentro de la respuesta JSON:


"author": "/users/:userId"


Manejar los errores con elegancia y devolver los códigos de error estándar

Para eliminar la confusión de los usuarios de la API cuando se produce un error, debemos manejar los errores con elegancia y devolver códigos de respuesta HTTP que indiquen qué tipo de error se ha producido. Esto da a los mantenedores de la API suficiente información para entender el problema que ha ocurrido. No queremos que los errores hagan caer nuestro sistema, así que podemos dejarlos sin manejar, lo que significa que el consumidor de la API tiene que manejarlos.


Los códigos de estado HTTP de error más comunes son:


  • 400 Bad Request (Mala solicitud) - Esto significa que la entrada del lado del cliente falla la validación.
  • 401 Unauthorized (No autorizado) - Esto significa que el usuario no está autorizado para acceder a un recurso. Normalmente se devuelve cuando el usuario no está autenticado.
  • 403 Forbidden (Prohibido) - Esto significa que el usuario está autenticado, pero no se le permite acceder a un recurso.
  • 404 Not Found (No encontrado) - Esto indica que no se encuentra un recurso.
  • 500 Internal server error (Error interno del servidor) - Este es un error genérico del servidor. Probablemente no debería ser lanzado explícitamente.
  • 502 Bad Gateway (Puerta de enlace mala) - Esto indica una respuesta no válida de un servidor ascendente.
  • 503 Service Unavailable (Servicio no disponible) - Esto indica que algo inesperado ocurrió en el lado del servidor (puede ser cualquier cosa como una sobrecarga del servidor, algunas partes del sistema fallaron, etc.).

Deberíamos lanzar los errores que correspondan al problema que ha encontrado nuestra aplicación. Por ejemplo, si queremos rechazar los datos de la carga útil de la solicitud, entonces deberíamos devolver una respuesta 400 como la siguiente en una API Express:


En el código anterior, tenemos una lista de usuarios existentes en la matriz de users con el correo electrónico dado. 


Entonces, si intentamos enviar la carga útil con el valor del email electrónico que ya existe en los users, obtendremos un código de estado de respuesta 400 con un mensaje de "El usuario ya existe" para que los usuarios sepan que el usuario ya existe. Con esa información, el usuario puede corregir la acción cambiando el correo electrónico a algo que no existe.


Los códigos de error deben ir acompañados de mensajes para que los mantenedores tengan suficiente información para solucionar el problema, pero los atacantes no pueden utilizar el contenido del error para llevar a cabo nuestros ataques, como robar información o hacer caer el sistema. Siempre que nuestra API no se complete con éxito, debemos fallar con gracia enviando un error con información para ayudar a los usuarios a realizar acciones correctivas.


Permitir el filtrado, la clasificación y la paginación

Las bases de datos detrás de una API REST pueden llegar a ser muy grandes. A veces, hay tantos datos que no deberían devolverse todos a la vez porque son demasiado lentos o harán caer nuestros sistemas. Por lo tanto, necesitamos formas de filtrar los elementos.


También necesitamos formas de paginar los datos para que sólo devolvamos unos pocos resultados a la vez. No queremos inmovilizar los recursos durante demasiado tiempo tratando de obtener todos los datos solicitados de una vez.


El filtrado y la paginación aumentan el rendimiento al reducir el uso de los recursos del servidor. Cuanto más datos se acumulen en la base de datos, más importantes serán estas funciones. He aquí un pequeño ejemplo en el que una API puede aceptar una cadena de consulta con varios parámetros de consulta para permitirnos filtrar elementos por sus campos:


En el código anterior, tenemos la variable req.query para obtener los parámetros de la consulta. A continuación, extraemos los valores de las propiedades desestructurando los parámetros de consulta individuales en variables utilizando la sintaxis de desestructuración de JavaScript. Por último, ejecutamos un filter con cada valor de los parámetros de consulta para localizar los elementos que queremos devolver.


Una vez hecho esto, devolvemos los results como respuesta. Por lo tanto, cuando hacemos una petición GET a la siguiente ruta con la cadena de consulta:


/employees?lastName=Smith&age=30


Lo conseguimos:


como la respuesta devuelta ya que filtramos por lastName y age.


Del mismo modo, podemos aceptar el parámetro de consulta page y devolver un grupo de entradas en la posición de (page - 1) * 20 a page * 20. También podemos especificar los campos por los que ordenar en la cadena de consulta. Por ejemplo, podemos obtener el parámetro de una cadena de consulta con los campos por los que queremos ordenar los datos. Entonces podemos ordenarlos por esos campos individuales. Por ejemplo, podemos querer extraer la cadena de consulta de una URL como:


http://example.com/articles?sort=+author,-datepublished


Donde + significa ascendente y - significa descendente. Así, ordenamos por nombre de autor en orden alfabético y por datepublished de más reciente a menos reciente.


Mantener buenas prácticas de seguridad

La mayor parte de la comunicación entre el cliente y el servidor debe ser privada, ya que a menudo enviamos y recibimos información privada. Por lo tanto, el uso de SSL/TLS para la seguridad es una necesidad.


Un certificado SSL no es demasiado difícil de cargar en un servidor y el coste es gratuito o muy bajo. No hay razón para no hacer que nuestras APIs REST se comuniquen a través de canales seguros en lugar de hacerlo en abierto.


La gente no debería poder acceder a más información de la que solicita. Por ejemplo, un usuario normal no debería poder acceder a la información de otro usuario. Tampoco deberían poder acceder a los datos de los administradores.


Para hacer cumplir el principio de mínimo privilegio, necesitamos añadir controles de roles, ya sea para un solo rol, o tener roles más granulares para cada usuario. Si elegimos agrupar a los usuarios en unos pocos roles, entonces los roles deben tener los permisos que cubren todo lo que necesitan y no más. Si tenemos permisos más granulares para cada característica a la que los usuarios tienen acceso, entonces tenemos que asegurarnos de que los administradores pueden añadir y eliminar esas características de cada usuario en consecuencia. Además, tenemos que añadir algunos roles preestablecidos que se pueden aplicar a un grupo de usuarios para que no tengamos que hacerlo para cada usuario manualmente.


Datos en caché para mejorar el rendimiento

Podemos añadir el almacenamiento en caché para devolver los datos desde la memoria caché local en lugar de consultar la base de datos para obtener los datos cada vez que queramos recuperar algún dato que los usuarios soliciten. Lo bueno de la caché es que los usuarios pueden obtener los datos más rápidamente. Sin embargo, los datos que los usuarios obtienen pueden estar desactualizados. Esto también puede conducir a problemas cuando se depura en entornos de producción cuando algo va mal ya que seguimos viendo datos antiguos.


Hay muchos tipos de soluciones de caché como Redis, caché en memoria, y más. Podemos cambiar la forma en que se almacenan los datos en caché según nuestras necesidades.


Por ejemplo, Express tiene el middleware de apicache para añadir caché a nuestra aplicación sin mucha configuración. Podemos añadir una simple caché en memoria en nuestro servidor de esta manera:


El código anterior sólo hace referencia al middleware de apicache con apicache.middleware y entonces tenemos:  app.use(cache('5 minutes')) para aplicar la caché a toda la aplicación. Por ejemplo, almacenamos en caché los resultados durante cinco minutos. Podemos ajustar esto a nuestras necesidades.


Si está utilizando el almacenamiento en caché, también debería incluir la información Cache-Control en sus cabeceras. Esto ayudará a los usuarios a utilizar eficazmente su sistema de caché.


Versionar nuestras APIs

Deberíamos tener diferentes versiones de la API si hacemos algún cambio en ellas que pueda romper los clientes. El versionado puede hacerse según la versión semántica (por ejemplo, 2.0.6 para indicar la versión mayor 2 y el sexto parche) como hacen la mayoría de las apps hoy en día.


De este modo, podemos ir eliminando gradualmente los puntos finales antiguos en lugar de obligar a todo el mundo a pasarse a la nueva API al mismo tiempo. El punto final v1 puede permanecer activo para las personas que no quieren cambiar, mientras que el v2, con sus nuevas y brillantes características, puede servir a aquellos que están listos para actualizar. Esto es especialmente importante si nuestra API es pública. Deberíamos versionarlas para no romper las aplicaciones de terceros que utilizan nuestras APIs.


El versionado se suele hacer con /v1/, /v2/, etc. añadidos al principio de la ruta de la API. Por ejemplo, podemos hacerlo con Express de la siguiente manera:


Sólo tenemos que añadir el número de versión al comienzo de la ruta de la URL del punto final para versionarlos.


Conclusión

Lo más importante para diseñar APIs REST de alta calidad es tener consistencia siguiendo los estándares y convenciones de la web. JSON, SSL/TLS y los códigos de estado HTTP son elementos estándar de la web moderna. El rendimiento también es una consideración importante. Podemos aumentarlo no devolviendo demasiados datos a la vez. Además, podemos utilizar la caché para no tener que consultar los datos todo el tiempo.


Las rutas de los endpoints deben ser consistentes, utilizamos sólo sustantivos ya que los métodos HTTP indican la acción que queremos realizar. Las rutas de los recursos anidados deben venir después de la ruta del recurso padre. Deberían decirnos qué estamos obteniendo o manipulando sin necesidad de leer documentación extra para entender lo que está haciendo.


Obten el codigo utilizado aqui


Reactions

3

0

0

1

Access hereTo be able to comment

TheWhiteCode.com is not the creator or owner of the images shown, references are: