Consejos y mejores prácticas de desarrollo de API REST - Parte 2

Consejos y mejores prácticas de desarrollo de API REST - Parte 2

@programacion

EL ESQUEMA

Si bien su código API puede ser súper limpio y brillante, es muy probable que sus consumidores nunca lo vean. Sin embargo, lo que se cansarán de ver es el esquema de su API.

Debe intentar mantener su esquema limpio utilizando nombres claros que se expliquen por sí mismos y una estructura lo más cercana posible a la auto-documentación. Además, con unas pocas reglas simples, debería poder probarlo en el futuro para poder seguir agregando más información a las respuestas existentes sin tener que desaprobar su versión principal antes de tiempo.

1. Sugerencias de implementación de esquemas

Los errores relacionados con el esquema más comunes que he encontrado a lo largo de los años provienen de la falta de rigor en el entorno de desarrollo de API. Al diseñar su API, el uso de un marco adecuado para su lenguaje de programación es muy útil para ayudarlo a hacer las cosas de la manera correcta.

Tómese su tiempo para estudiar las opciones disponibles y tenga en cuenta que, si bien la mayoría de los marcos web brindan herramientas para ayudarlo a implementar una API REST, algunos dejarán gran parte del trabajo en sus manos. No reinventes la rueda.

Mantenga una capa de modelo de datos estricta

La mayoría de las tecnologías web de back-end (bueno, front-end también para el caso) se basan en lenguajes de programación de secuencias de comandos de tipo flexible, que tienen un historial de malos hábitos de programación de los que es difícil deshacerse. PHP y Javascript (en forma de Node.js) son los culpables más comunes de las inconsistencias de esquema, ya que se usan con mucha frecuencia sin una infraestructura de modelo de datos adecuada.

Asegúrese de que su capa de datos interna esté estrictamente modelada utilizando un ORM para su base de datos u otra infraestructura de modelo de datos. Sus respuestas deben estar compuestas por modelos comerciales de datos anidados que estén estrictamente definidos en su código. No confíe en los resultados de las consultas de la base de datos asignadas a diccionarios simples generados en tiempo de ejecución u otros enfoques similares. Mantener una capa de modelo estricta es lo que debería hacer en todas sus aplicaciones de todos modos.

Sus modelos de datos siempre deben filtrarse antes de exponerse a las respuestas de la API para evitar filtrar información confidencial. Cualquier cambio en los campos que se exponen a la API debe evaluarse para su compatibilidad con la versión actual de la API y agregarse al registro de cambios de la API.

2. Buenas prácticas de esquemas

https://unsplash.com/photos/Mpq0LddqiTk

a) Nomenclatura coherente

Su nomenclatura debe ser significativa y coherente en toda su API, tanto en los URI como en los campos de solicitud y respuesta. Las ambigüedades en la nomenclatura causan mucho estrés a otros desarrolladores, así como a usted mismo cuando regresa a su código después de un tiempo.

Recuerde que sus URI y su esquema deben comunicar su propósito a sus consumidores de la manera más clara posible sin que tengan que navegar constantemente por su documentación. No sea reacio a usar nombres prolijos para conceptos difíciles siempre y cuando no se exceda.

b) Tipos de datos estrictos

Su esquema siempre debe estar escrito estrictamente, incluso si el lenguaje de programación de su elección no lo está. Los campos numéricos siempre deben contener números, los campos de cadena siempre deben contener cadenas, etc. Nunca debe mezclar diferentes tipos de datos en el mismo campo. Sus valores pueden fluctuar, pero sus tipos de datos no deberían.

Es un error muy común en las API creadas en entornos de tipo flexible ver un campo que contiene el valor numérico 42 en una respuesta y el valor de cadena "42" en otra. Esta práctica es inconsistente y difícil de analizar de forma segura en todos los clientes. En un esquema de tipo flexible, los clientes deben tomar algunas decisiones muy peligrosas con respecto a su indulgencia al analizar cada campo individual.

Obviamente, esto no solo se aplica a tipos de datos primitivos (números, cadenas, booleanos, etc.) sino también a objetos JSON y matrices. No devuelva un objeto de tipo Charen un campo que contenga un objeto de tipo Table, o una matriz de Cars en un campo que contenga una matriz de Bicycles.

A pesar de que lo anterior parece un conocimiento realmente básico, se sorprenderá de cuántos buenos desarrolladores se equivocan y son víctimas de debilidades elementales en su entorno de desarrollo.

Una capa de modelo sólida para su implementación lo ayuda a evitar errores tan vergonzosos.

c) No omita campos

Cuando no haya ningún valor disponible para un determinado campo, no omita ese campo por completo. Utilice nulluna cadena vacía , una matriz vacía o cero , según el tipo de datos y la semántica del valor perdido.

No dificulte que las personas comprendan su esquema pidiéndoles que realicen 10 solicitudes para obtener todos los campos cuando una solicitud debería ser suficiente. Tal vez puedan buscarlo en la documentación, pero ¿por qué no hacerlo más fácil para ellos y para usted y hacer que el esquema sea fácilmente comprensible y cercano a la auto-documentación? Recuerde, la documentación generalmente se vuelve obsoleta mucho más rápido que el código y el código es lo que genera su esquema.

Nuevamente, este tipo de error es mucho más fácil de evitar si usa una capa de modelo sólida en su implementación.

d) No abuse de los objetos JSON

He visto esto más veces de las que puedo recordar tanto en solicitudes de API como en respuestas. Esto también se debe a las malas prácticas comúnmente asociadas con los lenguajes de tipo impreciso.

Supongamos que tiene un objeto people con identificadores únicos como claves y subobjetos como valores:

Los campos del objeto people cambian cada vez que cambia su contenido anidado. En esta llamada, que tiene los campos 12345678 pero nadie sabe lo que sus campos serán nombrados en la siguiente llamada.

Esta es una práctica terrible y genera un código inconsistente y generalmente terrible cuando se analiza en cualquier lenguaje escrito estrictamente. Cada objeto JSON en su API siempre debe tener un conjunto de campos inmutable y estrictamente definido entre las solicitudes.

Lo anterior es un excelente caso de uso para matrices. Simplemente devuelva una matriz e incluya la identificación dentro de cada elemento de la matriz.

Así es como debería verse el objeto de people:

Por lo general, este tipo de abuso de esquema ocurre cuando intenta facilitar las búsquedas en su código de backend mediante el uso de un diccionario con claves de identificación única. Debe tener en cuenta que este es un caso en el que un detalle de implementación interna se está filtrando a sus usuarios, un fenómeno que debe evitar en todos los aspectos del desarrollo de software.

Probablemente se esté cansando de leer esto, pero estos problemas también se pueden evitar con una infraestructura de modelo sólida.

e) No abuse de las matrices JSON

Entonces, ha seguido los consejos anteriores y ha cambiado algunos de sus objetos a matrices. ¡Buen trabajo!

Ahora debe asegurarse de que sus matrices contengan solo un tipo de objeto . ¡No mezcle manzanas y naranjas! Recuerde, no todos los clientes utilizan contenedores de tipos imprecisos para sus datos, y analizar listas de recursos heterogéneos no solo es incoherente y molesto, ¡sino también inseguro!

Cuando no pueda evitar en absoluto devolver diferentes tipos de entidades en la misma matriz, intente devolver una lista de superobjetos que sean lo suficientemente abstractos para describir los atributos de todos los tipos de objetos que necesita devolver.

En el ejemplo de manzanas y naranjas, tal vez debería devolver objetos de tipo Fruit. Un Fruitpuede contener todos los atributos de los objetos AppleOrange , así como un campo type que especifica exactamente a qué tipo de fruta se refiere cada objeto.

Si los atributos de los elementos que devuelve son completamente dispares para cada tipo devuelto, pero aún tiene que devolverlos en la misma lista, es posible que deba recurrir a medidas "extremas" como los objetos contenedores. Esta no es una solución muy elegante, pero hay algunos casos extremos que la hacen necesaria.

A continuación, se muestra un ejemplo de objetos contenedores:

Su punto final devuelve una lista de vehículos voladores que posee una persona, así como algunas características básicas de cada uno de ellos. Los vehículos voladores pueden ser aviones o globos aerostáticos y son bastante diferentes entre sí. Semánticamente, tiene poco sentido agregar atributos como envergadura , número de motores o caballos de fuerza a un globo, de la misma manera que tiene poco sentido agregar atributos como canasta , material de globo y forma de globo a un avión.

Agregar todos esos campos de atributos a un solo tipo de objeto sería redundante. En su lugar, puede mantener su avión y objetos de globo agradables y limpios y guardarlos dentro de objetos contenedores simples .

En este caso, un objeto contenedor de tipo Vehicle(esencialmente un supertipo ) tendría un campo type (con un valor de "avión" o "globo") , un campo airplane y un campo balloon. Al devolver un objeto Airplane, se establece el objeto contenedor para el Tipo “avión”  y luego se rellenan de su campo airplanecon el objeto Airplane y el campo balloon con un valor null (recuerde que debe volver siempre todos los campos - incluso cuando están vacíos ).

Nuevamente, este es un diseño que debe evitar si es posible, pero si realmente tiene que devolver objetos completamente diferentes en la misma colección, los objetos contenedores son una buena manera de mantener un esquema estrictamente escrito de una manera (más o menos) limpia.

f) No confíe en mensajes de error simples codificados

Odio decepcionarlo, pero no importa cuán divertidos e ingeniosos sean sus mensajes de error, rara vez llegarán a los ojos de sus usuarios finales. No es que otros desarrolladores no aprecien tus habilidades de escritura, es solo que nunca sabes cómo un cliente tendrá que presentar un error. Puede ser una ventana emergente con muchas líneas de texto, un breve mensaje de brindis, un borde rojo alrededor de un campo de texto o incluso un sonido que transmita el significado del error de una manera completamente diferente.

Además, siempre debe devolver sus errores de una manera sucinta y legible por máquina para que sean fáciles de analizar y distinguir entre sí por sus aplicaciones cliente. Debe devolver los códigos de estado HTTP adecuados e incluir información adicional específica del error (como códigos internos e información adicional) en un objeto de error estrictamente definido en el cuerpo de la respuesta.

He aquí un ejemplo:

Su consumidor solicita un pedido determinado para un cliente determinado a través del siguiente punto final.

GET /customers/21/order/42

Si no se encuentra el cliente o el pedido, la respuesta debe tener un código de estado 404 "No encontrado" , pero ¿es suficiente? El cliente no podrá distinguir exactamente qué causó el error, porque no sabe qué es exactamente lo que no se encontró. ¿Fue el cliente o el pedido ?

Ahí es donde el objeto error en su cuerpo de respuesta resulta útil.

Un campo code legible por máquina dentro de su objeto de error hace que las cosas sean más específicas para los clientes. Además, convertirlo en una cadena (por ejemplo, "customer_not_found" ) en lugar de un número, facilita mucho las cosas a los desarrolladores, que no tienen que navegar por una tabla de números y descripciones de errores en la documentación de su API.

Finalmente, un campo message podría explicar mejor el motivo del error a los desarrolladores, para que tengan una mejor idea de cómo manejarlo y dónde buscar información adicional. Idealmente, sus errores deben localizarse según el encabezado Accept-Language de las solicitudes de sus clientes. Quién sabe, tal vez de esa manera los usuarios finales puedan leer sus obras maestras en algún momento.

g) No use enumeraciones numéricas

Como se mencionó una y otra vez, debe esforzarse por lograr un esquema de fácil lectura y auto-documentación. No use números para casos de enumeración. Utilice cadenas simples.

¿Tiene un campo type en su objeto Animal? No utilice 12345como sus valores. “dog”“cat”“parrot”“armadillo”“elephant”son mucho más fáciles de leer por los seres humanos y hacer casi ninguna diferencia a una máquina que sabe cómo comparar cadenas.

La gente suele hacer esto cuando tienen enumeraciones numéricas que se utilizan internamente en el backend, pero ese es (nuevamente) un detalle de implementación que no debe filtrarse a los consumidores de API.

También he escuchado excusas como un mayor consumo de ancho de banda del enfoque de cadenas, pero hay otras formas mejores de lidiar con ese problema. Su esquema debe ser lo suficientemente detallado para que sea fácil de entender con una sola mirada y debe reducir el consumo de ancho de banda con Gzip , lo que hace una gran diferencia en comparación con el ahorro de unos pocos bytes mediante el uso de enumeraciones numéricas.

h) No devuelva matrices JSON no encapsuladas

¿Qué es exactamente la encapsulación (o sobres JSON) en este contexto? Explicado de manera simple, significa envolver (o envolver) los datos de su respuesta en un objeto JSON y devolverlo en un campo data(u otro, con un nombre similar) en la raíz del cuerpo de su respuesta.

Algunas personas parecen pensar que esta es una buena práctica para todas las respuestas, porque le permite agregar campos de metadatos en el futuro (como información de error o paginación), sin alterar el objeto de respuesta principal. Aunque eso puede requerir un poco más de código al analizar, hace que el esquema de API sea un poco más limpio.

Incluso si no desea hacer esto para todas las respuestas, creo que es extremadamente útil (incluso necesario) cuando devuelve una colección de objetos. En tales casos, ¡nunca debe dejar una matriz como contenedor raíz de su respuesta!

La principal justificación para lo anterior es que si su contenedor raíz es una matriz JSON, su esquema cambia radicalmente cuando la respuesta necesita devolver un error que inevitablemente será un objeto JSON. Eso hace que el análisis sea más complejo sin proporcionar ningún beneficio real.

Además, (incluso si ignora lo anterior), una matriz hace que la desaprobación anticipada de su API sea aún más probable porque nunca se puede cambiar o enmendar de ninguna manera sin desaprobar su esquema. Por otro lado, usar un objeto como contenedor de respuesta raíz le permite agregar tantos campos como desee más adelante, sin causar desaprobación. Demonios, incluso podría devolver un tipo diferente y actualizado de objetos en una nueva matriz, siempre que se asegure de mantener la matriz anterior.

i) Utilice marcas de tiempo Unix o fechas ISO-8601

Mi preferencia personal siempre ha sido representar las fechas como marcas de tiempo de Unix en las respuestas, debido a que son relativamente cortas y muy fáciles de analizar. Sin embargo, a menos que sea una máquina, no son fáciles de convertir y, de hecho, se leen como fechas reales. Las fechas ISO-8601, por otro lado, son mejores desde el punto de vista de la legibilidad, pero son un poco más difíciles de analizar (aunque no mucho).

Cualquier otro formato de cadena además de estos dos debe evitarse a toda costa, ya que puede crear ambigüedades en el momento del análisis. Sé que puede especificar su propio formato de fecha y hora en su documentación y los clientes pueden analizar las fechas en función de eso, pero recuerde: su API debe ser lo más fácil de entender posible sin mucha ayuda externa.

j) Evite los objetos planos enormes

Si el objeto Ano es un usuario; y sin embargo, contiene campos tales como user_iduser_nameuser_favorite_coloruser_pety así sucesivamente, tal vez es hora de usar sólo un encapsulado objeto User dentro de objeto A.

Dado que los esquemas (tanto de base de datos como de API) tienden a volverse más complejos con el tiempo, es bueno intentar normalizarlos y mantenerlos lo más limpios posible desde el principio.

Si cree que anidar todo el objeto relacionado es excesivo, puede filtrar los campos que no son relevantes para la respuesta actual. Si el objeto anidado es importante para sus consumidores en algún otro contexto, lo más probable es que lo proporcione un recurso dedicado de su API. En ese caso, sus consumidores pueden recuperarlo en su totalidad si se asegura de proporcionar un enlace de hipermedia (cuando use HATEOAS ) o su clave principal.

k) Use booleanos JSON para (duh…) valores booleanos

Esto no hace una gran diferencia, pero ¿por qué usarlo 10cuando JSON ya tiene un tipo booleano? Es 2019 y ahora tenemos las palabras clave truefalse.

Además, usar 1 y 0 implica que también se puede esperar un 3, ¡o incluso uno menos!

Si lo hizo a propósito, pensando que podría necesitar más valores en el futuro, tal vez debería usar una enumeración de cadena en lugar de números y deshacerse del enfoque true/false de todos modos.

l) Utilice objetos para campos que podrían necesitar más información en el futuro

Haga siempre todo lo posible para preparar su API para el futuro de la forma que pueda. Intente anticipar los atributos que probablemente requerirán información adicional en el futuro, de modo que pueda aumentar la vida útil de su versión principal.

https://pixabay.com/es/photos/puertas-opciones-elija-decisi%c3%b3n-1767562/

He aquí un ejemplo:

Digamos que tienes un booleano is_available para cada uno de tus objetos Book. Si bien eso es suficiente para hacernos saber que un libro no está disponible cuando está false, no nos dice por qué no está disponible o cuándo volverá a estar disponible. En el futuro, si desea agregar esa información, tendrá que contaminar la raíz del objeto Book agregando los dos campos adicionales.

Un enfoque más limpio habría sido utilizar un campo availability, que almacena un objeto Availability, que al principio contiene solo el campo is_available, pero puede modificarse para contener información adicional sobre la disponibilidad del libro (como la razón por la que el libro no está disponible y una marca de tiempo de cuándo estará disponible nuevamente) sin desaprobar su esquema (más de eso en la siguiente sección).

3. Desactivación del esquema

Si está utilizando un enfoque de control de versiones que permite pequeños cambios incrementales en su API al tiempo que garantiza la estabilidad estructural, debe tener mucho cuidado cada vez que cambie las cosas.

Ciertos cambios en su esquema significan la desaprobación instantánea de su versión principal actual. Debe evitarlos a menos que sea absolutamente necesario. Aunque pueden ser obvios, los encuentro útiles como una lista de verificación de cosas que se deben mantener constantes al actualizar su esquema.

Tenga en cuenta que los cambios que se enumeran a continuación no son las únicas causas de la desaprobación del esquema (que a menudo puede deberse a detalles en su diseño específico), solo los más comunes.

a) No elimine campos ni cambie los nombres de los campos

Nunca puede estar seguro de cómo sus consumidores utilizan su información. No importa cuán insignificante o redundante pueda parecer un campo, si cometió el error de enviarlo a producción, entonces se quedará con él hasta la próxima versión principal. Obviamente, cambiar el nombre de un campo es lo mismo que eliminarlo.

b) No cambie los tipos de datos

Los tipos de datos deben permanecer estrictos no solo en las respuestas, sino también en las versiones menores. Esta es otra mala práctica relacionada con los entornos de desarrollo de tipo flexible.

No puede cambiar el tipo de datos de un campo sin desaprobar la versión actual. Si cometió el error de pasar un número como una cadena en su respuesta cuando lo envió por primera vez, siempre debe devolverlo como una cadena hasta la próxima versión principal.

c) No edite los casos de enumeración existentes

Devolver números (recuerde, no debería ) o cadenas como casos de enumeración es bastante común.

Por ejemplo, puede que esté utilizando los casos: “car”“truck”“motrcycle”para el campo vehicle_type.

Si notó el error tipográfico “motrcycle”, entiendo lo frustrado que debe sentirse, ¡pero no puede solucionarlo! Sus consumidores dependen de la versión mal escrita y estarán tristes de que se vaya. Sin embargo, puede hacerlo en la próxima versión principal (y no olvide agregarlo al registro de cambios).

d) No cambie los códigos de error de los puntos finales existentes

No. Sus aplicaciones cliente dependen de las antiguas. Es posible que sepan o no qué hacer con los nuevos, pero en cualquier caso, no puede estar seguro.


Report Page