REST API - procesamiento de archivos (es decir, imágenes) - mejores prácticas

194

Estamos desarrollando un servidor con REST API, que acepta y responde con JSON. El problema es que si necesita cargar imágenes del cliente al servidor.

Nota: y también estoy hablando de un caso de uso en el que la entidad (usuario) puede tener múltiples archivos (carPhoto, licensePhoto) y también tener otras propiedades (nombre, correo electrónico ...), pero cuando crea un nuevo usuario, no No envíe estas imágenes, se agregan después del proceso de registro.


Conozco las soluciones, pero cada una de ellas tiene algunos defectos.

1. Use multipart / form-data en lugar de JSON

bueno : las solicitudes POST y PUT son tan RESTful como sea posible, pueden contener entradas de texto junto con el archivo.

contras : ya no es JSON, que es mucho más fácil de probar, depurar, etc., en comparación con multipart / form-data

2. Permitir actualizar archivos separados

La solicitud POST para crear un nuevo usuario no permite agregar imágenes (lo cual está bien en nuestro caso de uso, como dije al principio), la carga de imágenes se realiza mediante solicitud PUT como datos multiparte / formulario a, por ejemplo, / users / 4 / carPhoto

bueno : todo (excepto el archivo que se carga) permanece en JSON, es fácil de probar y depurar (puede registrar solicitudes JSON completas sin tener miedo de su longitud)

contras : No es intuitivo, no puede POST o PUT todas las variables de entidad a la vez y también esta dirección /users/4/carPhotopuede considerarse más como una colección (el caso de uso estándar para REST API se ve así /users/4/shipments). Por lo general, no puede (y no desea) OBTENER / PONER cada variable de entidad, por ejemplo usuarios / 4 / nombre. Puede obtener el nombre con GET y cambiarlo con PUT en users / 4. Si hay algo después de la identificación, generalmente es otra colección, como users / 4 / reviews

3. Use Base64

Envíalo como JSON pero codifica archivos con Base64.

bueno : igual que la primera solución, es un servicio lo más RESTful posible.

Contras : una vez más, las pruebas y la depuración son mucho peores (el cuerpo puede tener megabytes de datos), hay un aumento en el tamaño y también en el tiempo de procesamiento tanto en el cliente como en el servidor


Realmente me gustaría usar la solución no. 2, pero tiene sus contras ... ¿Alguien me puede dar una mejor idea de la solución "cuál es la mejor"?

Mi objetivo es tener servicios RESTful con tantos estándares incluidos como sea posible, mientras quiero que sea lo más simple posible.

libik
fuente
También puede encontrar esto útil: stackoverflow.com/questions/4083702/…
Markon
55
Sé que este tema es antiguo, pero hemos enfrentado este problema recientemente. El mejor enfoque que tenemos es similar al tuyo número 2. Subimos archivos directamente a la API y luego adjuntamos estos archivos en el modelo. Con este escenario, puede crear imágenes de carga antes, después o en la misma página que el formulario, en realidad no importa. Buena discusión!
Tiago Matos
2
@TiagoMatos - sí, exactamente, lo describí en una respuesta que acepté recientemente
libik
66
Gracias por hacer esta pregunta.
Zuhayer Tahir
1
"también esta dirección / users / 4 / carPhoto se puede considerar más como una colección", no, no parece una colección y no necesariamente se consideraría como una. Está totalmente bien tener una relación con un recurso que no es una colección sino un recurso único.
B12Toaster

Respuestas:

152

OP aquí (estoy respondiendo esta pregunta después de dos años, la publicación hecha por Daniel Cerecedo no fue mala en ningún momento, pero los servicios web se están desarrollando muy rápido)

Después de tres años de desarrollo de software a tiempo completo (con enfoque también en arquitectura de software, gestión de proyectos y arquitectura de microservicios) definitivamente elijo la segunda forma (pero con un punto final general) como la mejor.

Si tiene un punto final especial para las imágenes, le da mucho más poder sobre el manejo de esas imágenes.

Tenemos la misma API REST (Node.js) para ambas aplicaciones móviles (iOS / Android) y frontend (usando React). Esto es 2017, por lo tanto, no desea almacenar imágenes localmente, desea cargarlas en algún almacenamiento en la nube (Google cloud, s3, cloudinary, ...), por lo tanto, desea un manejo general sobre ellas.

Nuestro flujo típico es que, tan pronto como selecciona una imagen, comienza a cargarse en el fondo (generalmente POST en / punto final de imágenes), devolviéndole la ID después de la carga. Esto es realmente fácil de usar, porque el usuario elige una imagen y luego normalmente procede con otros campos (es decir, dirección, nombre, ...), por lo tanto, cuando presiona el botón "enviar", la imagen ya está cargada. No espera y mira la pantalla diciendo "cargando ...".

Lo mismo vale para obtener imágenes. Especialmente gracias a los teléfonos móviles y los datos móviles limitados, no desea enviar imágenes originales, desea enviar imágenes redimensionadas, por lo que no ocupan tanto ancho de banda (y para que sus aplicaciones móviles sean más rápidas, a menudo no desea para cambiar su tamaño, desea que la imagen se ajuste perfectamente a su vista). Por esta razón, las buenas aplicaciones están usando algo como cloudinary (o tenemos nuestro propio servidor de imágenes para cambiar el tamaño).

Además, si los datos no son privados, envía de vuelta a la aplicación / interfaz solo URL y los descarga directamente desde el almacenamiento en la nube, lo que supone un gran ahorro de ancho de banda y tiempo de procesamiento para su servidor. En nuestras aplicaciones más grandes hay muchos terabytes descargados cada mes, no desea manejar eso directamente en cada uno de su servidor REST API, que se centra en la operación CRUD. Desea manejar eso en un solo lugar (nuestro Imageserver, que tiene almacenamiento en caché, etc.) o dejar que los servicios en la nube lo manejen todo.


Contras: Los únicos "contras" en los que debe pensar son "imágenes no asignadas". El usuario selecciona imágenes y continúa rellenando otros campos, pero luego dice "no" y apaga la aplicación o la pestaña, pero mientras tanto ha cargado correctamente la imagen. Esto significa que ha subido una imagen que no está asignada en ningún lado.

Hay varias formas de manejar esto. La más fácil es "No me importa", que es relevante, si esto no sucede muy a menudo o incluso desea almacenar cada imagen que el usuario le envíe (por cualquier motivo) y no desea ninguna supresión.

Otra es fácil también: tiene CRON y, es decir, todas las semanas y elimina todas las imágenes sin asignar de más de una semana.

libik
fuente
¿Qué sucederá si [tan pronto como seleccione la imagen, comience a cargarse en segundo plano (generalmente POST en / punto final de imágenes), devolviéndole la identificación después de cargar] cuando la solicitud falló debido a la conexión a Internet? ¿Avisará al usuario mientras continúa con otros campos (es decir, dirección, nombre, ...)? Apuesto a que aún esperará hasta que el usuario presione el botón "enviar" y vuelva a intentar su solicitud, haga que esperen mientras mira la pantalla que dice "uploadiing ...".
Adromil Balais
55
@AdromilBalais: la API RESTful no tiene estado, por lo tanto, no hace nada (el servidor no rastrea el estado del consumidor). El consumidor del servicio (es decir, la página web o el dispositivo móvil) es responsable de manejar las solicitudes fallidas, por lo tanto, el consumidor debe decidir si llama inmediatamente a la misma solicitud después de que esta falló o qué hacer (es decir, muestre el mensaje "Falló la carga de la imagen; desea volver a intentarlo ")
libik
2
Respuesta muy informativa y esclarecedora. Gracias por responder.
Zuhayer Tahir
Esto realmente no resuelve el problema inicial. Esto solo dice "use un servicio en la nube"
Martin Muzatko
3
@MartinMuzatko: lo hace, elige la segunda opción y le dice cómo debe usarla y por qué. Si quiere decir "pero esta no es la opción perfecta que le permite enviar todo en una sola solicitud y sin implicación", sí, desafortunadamente no existe tal solución.
libik
103

Hay varias decisiones que tomar :

  1. El primero sobre la ruta del recurso :

    • Modele la imagen como un recurso por sí solo:

      • Anidado en usuario (/ user /: id / image): la relación entre el usuario y la imagen se realiza implícitamente

      • En la ruta raíz (/ imagen):

        • El cliente es responsable de establecer la relación entre la imagen y el usuario, o;

        • Si se proporciona un contexto de seguridad con la solicitud POST utilizada para crear una imagen, el servidor puede establecer implícitamente una relación entre el usuario autenticado y la imagen.

    • Incruste la imagen como parte del usuario

  2. La segunda decisión es sobre cómo representar el recurso de imagen :

    • Como carga útil JSON codificada Base 64
    • Como una carga útil multiparte

Esta sería mi pista de decisión:

  • Por lo general, prefiero el diseño sobre el rendimiento a menos que haya un argumento sólido para ello. Hace que el sistema sea más fácil de mantener y que los integradores puedan entenderlo más fácilmente.
  • Entonces, mi primer pensamiento es buscar una representación Base64 del recurso de imagen porque le permite mantener todo JSON. Si elige esta opción, puede modelar la ruta de recursos como desee.
    • Si la relación entre el usuario y la imagen es de 1 a 1, preferiría modelar la imagen como un atributo, especialmente si ambos conjuntos de datos se actualizan al mismo tiempo. En cualquier otro caso, puede elegir libremente modelar la imagen como un atributo, actualizándola a través de PUT o PATCH, o como un recurso separado.
  • Si elige la carga útil de varias partes, me sentiría obligado a modelar la imagen como un recurso propio, de modo que otros recursos, en nuestro caso, el recurso del usuario, no se vean afectados por la decisión de usar una representación binaria para la imagen.

Luego viene la pregunta: ¿Hay algún impacto en el rendimiento al elegir base64 vs multiparte? . Podríamos pensar que el intercambio de datos en formato multiparte debería ser más eficiente. Pero este artículo muestra cuán poco difieren ambas representaciones en términos de tamaño.

Mi elección Base64:

  • Decisión de diseño consistente
  • Impacto insignificante en el rendimiento
  • Como los navegadores entienden los URI de datos (imágenes codificadas en base64), no hay necesidad de transformarlos si el cliente es un navegador
  • No votaré si tenerlo como un atributo o recurso independiente, depende de su dominio del problema (que no sé) y de su preferencia personal.
Daniel Cerecedo
fuente
3
¿No podemos codificar los datos utilizando otros protocolos de serialización como protobuf, etc.? Básicamente, estoy tratando de entender si hay otras formas más simples de abordar el aumento de tamaño y tiempo de procesamiento que viene con la codificación base64.
Andy Dufresne
1
Respuesta muy atractiva. Gracias por el enfoque paso a paso. Me hizo entender tus puntos mucho mejor.
Zuhayer Tahir
13

Su segunda solución es probablemente la más correcta. Debe usar la especificación HTTP y los tipos mime de la forma en que fueron diseñados y cargar el archivo a través de multipart/form-data. En cuanto al manejo de las relaciones, usaría este proceso (teniendo en cuenta que sé cero sobre sus supuestos o el diseño del sistema):

  1. POSTpara /userscrear la entidad de usuario.
  2. POSTla imagen a /images, asegurándose de devolver un Locationencabezado a donde se pueda recuperar la imagen según la especificación HTTP.
  3. PATCHa /users/carPhotoy asignarle el ID de la foto dada en el Locationencabezado de la etapa 2.
mmcclannahan
fuente
1
No tengo ningún control directo de "cómo el cliente usará la API" ... El problema de esto es que las imágenes "muertas" que no están parcheadas en algunos recursos ...
libik
44
Por lo general, cuando elige la segunda opción, se prefiere cargar primero el elemento de medios y devolver el identificador de medios al cliente, luego el cliente puede enviar los datos de la entidad, incluido el identificador de medios, este enfoque evita que las entidades dañadas o la información no coincida.
Kellerman Rivero
2

No hay una solución fácil. Cada camino tiene sus pros y sus contras. Pero la forma canónica es usar la primera opción: multipart/form-data. Como dice la guía de recomendaciones W3

El tipo de contenido "multipart / form-data" debe usarse para enviar formularios que contienen archivos, datos no ASCII y datos binarios.

Realmente no estamos enviando formularios, pero el principio implícito aún se aplica. Usar base64 como representación binaria es incorrecto porque está usando la herramienta incorrecta para lograr su objetivo, por otro lado, la segunda opción obliga a sus clientes API a hacer más trabajo para consumir su servicio API. Debe hacer el trabajo duro en el lado del servidor para proporcionar una API fácil de consumir. La primera opción no es fácil de depurar, pero cuando lo haces, probablemente nunca cambie.

Al multipart/form-datausarlo, estás apegado a la filosofía REST / http. Puede ver una respuesta a una pregunta similar aquí .

Otra opción si se mezclan las alternativas, puede usar datos multiparte / formulario, pero en lugar de enviar cada valor por separado, puede enviar un valor llamado carga útil con la carga útil json dentro de él. (Probé este enfoque usando ASP.NET WebAPI 2 y funciona bien).

Kellerman Rivero
fuente
2
Esa guía de recomendación W3 es irrelevante aquí, ya que está en el contexto de la especificación HTML 4.
Johann
1
Muy cierto ... ¿"datos no ASCII" requiere multiparte? En el siglo XXI? ¿En un mundo UTF-8? Por supuesto, esa es una recomendación ridícula para hoy. Incluso me sorprende que existiera en el HTML 4 días, pero a veces el mundo de la infraestructura de Internet se mueve muy lentamente.
Ray Toal