La web primitiva funcionaba con texto plano. Escribías un nombre de usuario o un comentario en un formulario, pulsabas enviar y una cadena corta viajaba al servidor a través de HTTP. Ese sencillo ciclo de solicitud-respuesta definía la infraestructura. Cuando la gente empezó a querer compartir fotos, documentos y vídeos, los ingenieros tuvieron que averiguar cómo mover datos binarios puros a través de un sistema construido íntegramente para texto legible.
Si intentas incluir un JPEG en un objeto JSON, te topas con un muro fundamental. JSON es un protocolo de texto. Espera caracteres Unicode válidos, comillas, llaves y cadenas debidamente escapadas. Un archivo binario es solo una larga secuencia de bytes, muchos de los cuales no tienen una representación imprimible. Si metes a la fuerza esos bytes en una cadena JSON, el analizador (parser) se rompe, las secuencias de escape corrompen la carga útil (payload) y todo el mensaje se vuelve ilegible al otro lado.
La codificación Base64 surgió como la solución alternativa obvia. Reasigna los datos binarios a un conjunto limitado de sesenta y cuatro caracteres ASCII imprimibles. Cada tres bytes binarios se convierten en cuatro caracteres de texto. La carga útil es ahora un JSON válido, lo que significa que sobrevivirá a un viaje a través de cualquier API estándar. Pero el coste es inmediato. Esa recodificación infla el tamaño del archivo aproximadamente un treinta y tres por ciento. Una imagen de tres megabytes se convierte en cuatro megabytes en la red. Tanto el cliente como el servidor consumen ciclos de CPU adicionales traduciendo los datos de un lado a otro. Más importante aún, muchos frameworks de servidores JSON leen todo el cuerpo en la memoria antes de analizarlo. Un puñado de cargas pesadas simultáneas puede saturar un servidor modesto porque cada una se mantiene en la RAM como una cadena de texto excesivamente pesada antes incluso de guardarse en el disco. Base64 funciona en un apuro, pero nunca fue diseñado para transportar archivos pesados a escala de producción.
La mejor respuesta es multipart/form-data. Este formato trata una única solicitud HTTP como una colección de partes separadas, cada una dividida por una cadena de delimitación (boundary) única. Una parte puede contener un campo de texto plano. La siguiente parte puede contener una imagen binaria pura, marcada con sus propios encabezados Content-Type y Content-Disposition. El servidor lee el flujo entrante de forma secuencial, buscando los marcadores de delimitación, y entrega cada sección al manejador (handler) correspondiente sin necesidad de tratar toda la carga útil como un único bloque de texto.
En Node.js, esta distinción es especialmente importante. El middleware express.json() sabe cómo analizar cuerpos JSON, pero no gestiona flujos de archivos (file streams). Para procesar cargas multipart, necesitas un analizador de flujo (streaming parser) como Multer o Busboy. Estas herramientas se conectan al flujo de la solicitud original y lo leen fragmento a fragmento. Multer, por ejemplo, te permite elegir si escribir los archivos entrantes en una carpeta temporal en el disco o mantener los más pequeños en memoria. Esa decisión de configuración es crucial. Si mantienes todo en memoria y tu aplicación recibe repentinamente varios archivos grandes a la vez, tu proceso puede quedarse sin espacio en el heap y colapsar. Escribir en el disco intercambia E/S (I/O) por estabilidad, pero introduce sus propias dudas sobre la limpieza y la seguridad de las rutas.
Para una aplicación pequeña, guardar archivos en una carpeta local como ./uploads resulta natural y rápido. El archivo llega a la misma máquina que ejecuta tu código, y servirlo de vuelta es solo cuestión de apuntar a la ruta correcta. Esto funciona perfectamente... hasta que deja de hacerlo.
En el momento en que colocas un equilibrador de carga (load balancer) delante de un segundo servidor de aplicaciones, el almacenamiento local se convierte en un error (bug). Un usuario sube una foto de perfil. El equilibrador de carga dirige la solicitud al Servidor A, y el archivo se escribe en el disco del Servidor A. Más tarde, ese usuario solicita ver la imagen, pero el equilibrador de carga envía la solicitud al Servidor B. El Servidor B comprueba su propio sistema de archivos y no encuentra nada. El archivo, efectivamente, ha desaparecido. Puedes implementar sesiones persistentes (sticky sessions) para vincular a un usuario a la misma máquina, pero es una solución frágil. Si el Servidor A se reinicia, se vuelve a desplegar o es reemplazado por una instancia de escalado automático (autoscaling), los datos se desvanecen. En entornos de contenedores, los discos locales son aún más efímeros. El sistema de archivos de un contenedor Docker está diseñado para ser desechable. Tratarlo como un almacenamiento permanente es una forma segura de perder los datos de los usuarios.
La solución estándar es separar el cómputo del almacenamiento. Mantienes tus servidores de aplicaciones sin estado (stateless) y envías los archivos subidos a un almacenamiento de objetos dedicado como AWS S3 o Google Cloud Storage. Estos servicios están diseñados para la durabilidad, la distribución geográfica y la concurrencia masiva. El servidor de aplicaciones gestiona la solicitud, valida los metadatos y luego entrega los bytes a una infraestructura diseñada específicamente para contenerlos.
Sin embargo, incluso este patrón crea un cuello de botella si se implementa de forma descuidada. Muchos equipos comienzan haciendo que el navegador suba el archivo al backend, y luego el backend reenvía cada byte al almacenamiento de objetos. Si un usuario sube un video de quinientos megabytes, su servidor se convierte en un intermediario. Consume ancho de banda al descargar el archivo y luego consume más ancho de banda al enviarlo a S3. La conexión permanece abierta durante toda la duración de la transferencia. Las subidas lentas de usuarios con malas condiciones de red pueden bloquear las conexiones del servidor durante minutos. El uso de memoria se mantiene elevado si el servidor almacena el flujo en un búfer y, si utiliza un hosting con facturación por consumo, estará pagando dos veces por la misma transferencia de datos. El escalado horizontal no resuelve esto, porque cada servidor adicional que agregue seguirá atrapado transportando bytes que no necesita ver.
Los sistemas modernos resuelven el problema eliminando por completo el backend de la ruta de datos. En lugar de aceptar el archivo, el backend solo acepta una solicitud de permiso para subirlo. El flujo es el siguiente:
- El navegador solicita al backend que inicie una subida, enviando generalmente solo el nombre del archivo, el tipo de archivo y el propósito previsto.
- El backend autentica al usuario, valida la solicitud según las reglas de negocio y utiliza un SDK para generar una URL presignada temporal del proveedor de almacenamiento de objetos.
- El backend devuelve esa URL al navegador. La URL está limitada a un bucket y una clave específicos, es válida por un breve periodo, como cinco o quince minutos, y está firmada con un token que otorga únicamente los permisos precisos necesarios.
- El navegador sube el archivo directamente a S3 o GCS utilizando un PUT o POST estándar. Los bytes viajan directamente desde el dispositivo del usuario hacia
