cover

Guía Completa para Subir Archivos desde el Cliente al Servidor


Escuchar este post

Selecciona una voz y genera audio para escuchar este post

Guía Completa para Subir Archivos: Del Cliente al Servidor en 2025

Developer working on file upload functionality

La subida de archivos sigue siendo una de las características más críticas en las aplicaciones web modernas. Ya sea que estés construyendo una plataforma de redes sociales, un sistema de gestión de documentos o un sitio de comercio electrónico, entender las sutilezas del manejo de archivos puede hacer o deshacer la experiencia del usuario.

¿Por Qué Dominar las Subidas de Archivos en 2025?

Las subidas de archivos modernas van más allá de simples solicitudes HTTP POST. Las aplicaciones de hoy demandan:

  • Progreso en Tiempo Real: Los usuarios esperan retroalimentación visual para cargas grandes
  • Resistencia: Capacidad de manejar interrupciones sin perder progreso
  • Rendimiento: Manejo eficiente de archivos de múltiples gigabytes
  • Seguridad: Protección contra cargas maliciosas y agotamiento de recursos
  • Simplicidad: Código mantenible y fácil de entender

1. FormData: La Solución Básica

FormData sigue siendo el enfoque más directo para cargas básicas de archivos. Es probado en batalla, universalmente soportado y perfecto para empezar:

// Carga básica de archivo con FormData async function uploadFile(fileInput) { const file = fileInput.files[0]; // Crear datos de formulario const formData = new FormData(); formData.append("file", file); formData.append("metadata", JSON.stringify({ uploadedAt: new Date().toISOString(), userId: getCurrentUserId() })); try { const response = await fetch("/api/upload", { method: "POST", body: formData // Nunca establecer Content-Type - el navegador lo hace automáticamente }); if (!response.ok) { throw new Error(`Upload failed: ${response.status}`); } const result = await response.json(); console.log("Upload successful:", result); return result; } catch (error) { console.error("Upload error:", error); throw error; } } // Servidor básico en React Router v7 export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const file = formData.get("file") as File; if (!file) { return Response.json({ error: "No file provided" }, { status: 400 }); } // Guardar archivo const buffer = Buffer.from(await file.arrayBuffer()); const fileName = `${Date.now()}-${file.name}`; const filePath = path.join("uploads", fileName); await fs.writeFile(filePath, buffer); return Response.json({ success: true, fileName, size: file.size, url: `/uploads/${fileName}` }); }

✅ Cuándo Usar FormData

  • Archivos menores a 10MB
  • Formularios simples con adjuntos
  • Prototipos rápidos y MVPs
  • Cuando no necesitas seguimiento de progreso

⚠️ Limitaciones

  • Sin visibilidad del progreso de carga
  • No maneja bien archivos muy grandes
  • Difícil manejar errores específicos
  • Carga todo en memoria antes de enviar

2. XMLHttpRequest: Añadiendo Seguimiento de Progreso

Para aplicaciones profesionales, mostrar el progreso de carga mejora dramáticamente la experiencia del usuario:

class UploadManager { constructor() { this.activeUploads = new Map(); } uploadWithProgress(file, options = {}) { const { onProgress = () => {}, onComplete = () => {}, onError = () => {}, endpoint = "/api/upload" } = options; return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const uploadId = crypto.randomUUID(); // Almacenar referencia para posible cancelación this.activeUploads.set(uploadId, xhr); const formData = new FormData(); formData.append("file", file); formData.append("uploadId", uploadId); // Seguimiento de progreso xhr.upload.addEventListener("progress", (event) => { if (event.lengthComputable) { const percentage = Math.round((event.loaded / event.total) * 100); onProgress({ loaded: event.loaded, total: event.total, percentage, speed: this.calculateSpeed(event.loaded, uploadId) }); } }); // Manejar finalización xhr.addEventListener("load", () => { this.activeUploads.delete(uploadId); if (xhr.status >= 200 && xhr.status < 300) { const response = JSON.parse(xhr.responseText); onComplete(response); resolve(response); } else { const error = new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`); onError(error); reject(error); } }); // Manejar errores de red xhr.addEventListener("error", () => { this.activeUploads.delete(uploadId); const error = new Error("Network error during upload"); onError(error); reject(error); }); // Manejar cancelación xhr.addEventListener("abort", () => { this.activeUploads.delete(uploadId); reject(new Error("Upload cancelled")); }); xhr.open("POST", endpoint); xhr.send(formData); // Almacenar tiempo de inicio para calcular velocidad this.startTimes = this.startTimes || new Map(); this.startTimes.set(uploadId, Date.now()); }); } calculateSpeed(loaded, uploadId) { const startTime = this.startTimes?.get(uploadId); if (!startTime) return 0; const elapsed = (Date.now() - startTime) / 1000; // segundos return loaded / elapsed; // bytes por segundo } cancelUpload(uploadId) { const xhr = this.activeUploads.get(uploadId); if (xhr) { xhr.abort(); this.activeUploads.delete(uploadId); this.startTimes?.delete(uploadId); } } } // Implementación en React function FileUploadComponent() { const [progress, setProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); const [uploadSpeed, setUploadSpeed] = useState(0); const uploadManager = useRef(new UploadManager()); const currentUploadId = useRef(null); const formatSpeed = (speed) => { const mb = speed / (1024 * 1024); return `${mb.toFixed(1)} MB/s`; }; const handleUpload = async (event) => { const file = event.target.files[0]; if (!file) return; setIsUploading(true); setProgress(0); try { const result = await uploadManager.current.uploadWithProgress(file, { onProgress: (data) => { setProgress(data.percentage); setUploadSpeed(data.speed); currentUploadId.current = data.uploadId; }, onComplete: (response) => { console.log("Upload completado:", response); setIsUploading(false); setProgress(100); }, onError: (error) => { console.error("Error en upload:", error); setIsUploading(false); setProgress(0); } }); } catch (error) { console.error("Failed to upload:", error); setIsUploading(false); } }; const handleCancel = () => { if (currentUploadId.current) { uploadManager.current.cancelUpload(currentUploadId.current); } }; return ( <div className="upload-container"> <input type="file" onChange={handleUpload} disabled={isUploading} accept="image/*,video/*,.pdf" /> {isUploading && ( <div className="upload-progress"> <div className="progress-bar"> <div className="progress-fill" style={{ width: `${progress}%` }} /> <span className="progress-text">{progress}%</span> </div> <div className="upload-info"> <span>Velocidad: {formatSpeed(uploadSpeed)}</span> <button onClick={handleCancel} className="cancel-btn"> Cancelar </button> </div> </div> )} </div> ); }

🎯 Cuándo Usar XMLHttpRequest

  • Archivos entre 10MB - 100MB
  • Cuando necesitas mostrar progreso detallado
  • Aplicaciones que requieren cancelación de uploads
  • Dashboards profesionales y herramientas de administración

3. Streams Modernos: La Solución 2025

Cloud storage and file transfer concept

Con las APIs de streaming modernas, subir archivos grandes es increíblemente más simple y eficiente:

// Upload directo con streaming nativo async function uploadWithStream(file, options = {}) { const { onProgress = () => {}, endpoint = "/api/upload-stream" } = options; // Crear stream del archivo const fileStream = file.stream(); // Progress tracking con TransformStream let loaded = 0; const startTime = Date.now(); const progressStream = new TransformStream({ transform(chunk, controller) { loaded += chunk.byteLength; const elapsed = (Date.now() - startTime) / 1000; const speed = loaded / elapsed; onProgress({ loaded, total: file.size, percentage: Math.round((loaded / file.size) * 100), speed }); controller.enqueue(chunk); } }); // Pipe el stream a través del transformer const trackedStream = fileStream.pipeThrough(progressStream); return fetch(endpoint, { method: "POST", body: trackedStream, duplex: "half", // Required para streaming headers: { 'Content-Type': file.type, 'Content-Length': file.size.toString(), 'X-File-Name': encodeURIComponent(file.name) } }); } // Implementación React moderna con streams function ModernFileUpload() { const [progress, setProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); const [uploadSpeed, setUploadSpeed] = useState(0); const abortController = useRef(null); const formatBytes = (bytes) => { const mb = bytes / (1024 * 1024); return `${mb.toFixed(1)} MB`; }; const formatSpeed = (speed) => { const mbps = speed / (1024 * 1024); return `${mbps.toFixed(1)} MB/s`; }; const handleUpload = async (event) => { const file = event.target.files[0]; if (!file) return; setIsUploading(true); setProgress(0); abortController.current = new AbortController(); try { const response = await uploadWithStream(file, { onProgress: (data) => { setProgress(data.percentage); setUploadSpeed(data.speed); } }); if (response.ok) { const result = await response.json(); console.log("Upload completado:", result); } else { throw new Error(`Upload failed: ${response.status}`); } } catch (error) { if (error.name !== 'AbortError') { console.error("Upload error:", error); } } finally { setIsUploading(false); setProgress(0); setUploadSpeed(0); } }; const cancelUpload = () => { abortController.current?.abort(); }; return ( <div className="modern-upload"> <div className="upload-area"> <input type="file" onChange={handleUpload} disabled={isUploading} accept="*/*" /> <div className="upload-hint"> Selecciona archivos hasta 500MB </div> </div> {isUploading && ( <div className="upload-status"> <div className="progress-section"> <div className="progress-bar"> <div className="progress-fill" style={{ width: `${progress}%` }} /> </div> <div className="progress-details"> <span>{progress}%</span> <span>{formatSpeed(uploadSpeed)}</span> </div> </div> <button onClick={cancelUpload} className="cancel-button"> Cancelar Upload </button> </div> )} </div> ); }

🚀 Ventajas de los Streams Modernos

  • 90% menos código comparado con implementaciones legacy
  • Mejor rendimiento con menos uso de memoria
  • Cancelación nativa con AbortController
  • Progress tracking integrado sin polling manual
  • Backpressure automático para evitar sobrecarga
  • Escalabilidad para archivos de cualquier tamaño

Servidor Moderno para Streams (React Router v7)

Implementación que recibe streams directamente:

// app/routes/api/upload-stream.tsx import { ActionFunctionArgs } from "react-router"; import { createWriteStream } from "fs"; import { mkdir } from "fs/promises"; import { Readable } from "stream"; import { pipeline } from "stream/promises"; import path from "path"; export async function action({ request }: ActionFunctionArgs) { const contentLength = request.headers.get("content-length"); const fileName = decodeURIComponent( request.headers.get("x-file-name") || "upload" ); // Validaciones básicas if (!contentLength) { return Response.json( { error: "Content-Length header required" }, { status: 400 } ); } const fileSize = parseInt(contentLength); // Límite de 500MB if (fileSize > 500 * 1024 * 1024) { return Response.json( { error: "File too large (max 500MB)" }, { status: 413 } ); } // Crear directorio de uploads const uploadDir = path.join(process.cwd(), "uploads"); await mkdir(uploadDir, { recursive: true }); const safeFileName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9.-]/g, '_')}`; const filePath = path.join(uploadDir, safeFileName); try { // Convertir Request body a Node.js Readable stream const bodyStream = Readable.fromWeb(request.body as ReadableStream); const fileWriteStream = createWriteStream(filePath); // Pipeline directo del stream de entrada al archivo await pipeline(bodyStream, fileWriteStream); return Response.json({ success: true, fileName: safeFileName, size: fileSize, url: `/uploads/${safeFileName}`, uploadedAt: new Date().toISOString() }); } catch (error) { console.error("Stream upload error:", error); // Cleanup del archivo en caso de error try { await fs.unlink(filePath); } catch {} return Response.json( { error: "Upload failed" }, { status: 500 } ); } }

Validación y Seguridad

Implementa validación tanto en cliente como servidor:

class FileValidator { constructor(config = {}) { this.maxSize = config.maxSize || 100 * 1024 * 1024; // 100MB this.allowedTypes = config.allowedTypes || [ "image/jpeg", "image/png", "image/webp", "video/mp4", "video/webm", "application/pdf" ]; } validate(file) { const errors = []; // Validación de tamaño if (file.size > this.maxSize) { errors.push({ code: "FILE_TOO_LARGE", message: `Archivo muy grande (máximo ${this.formatBytes(this.maxSize)})` }); } // Validación de tipo MIME if (!this.allowedTypes.includes(file.type)) { errors.push({ code: "INVALID_TYPE", message: `Tipo de archivo no permitido: ${file.type}` }); } // Validación de extensión const extension = file.name.split('.').pop()?.toLowerCase(); const validExtensions = ['jpg', 'jpeg', 'png', 'webp', 'mp4', 'webm', 'pdf']; if (!extension || !validExtensions.includes(extension)) { errors.push({ code: "INVALID_EXTENSION", message: `Extensión no válida: .${extension}` }); } // Verificar que no haya caracteres sospechosos if (file.name.includes('../') || file.name.includes('..\\')) { errors.push({ code: "SUSPICIOUS_FILENAME", message: "Nombre de archivo contiene caracteres sospechosos" }); } return { valid: errors.length === 0, errors }; } formatBytes(bytes) { const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } } // Uso de la validación const validator = new FileValidator({ maxSize: 50 * 1024 * 1024, // 50MB allowedTypes: ["image/jpeg", "image/png", "video/mp4"] }); const validation = validator.validate(file); if (!validation.valid) { console.error("Validation errors:", validation.errors); // Mostrar errores al usuario return; }

Recomendaciones por Caso de Uso

Archivos Pequeños (< 10MB)

  • Tecnología: FormData + fetch
  • Ejemplos: Avatares, documentos PDF, imágenes de blog
  • Ventaja: Simplicidad máxima

Aplicaciones Profesionales (10MB - 100MB)

  • Tecnología: XMLHttpRequest con progreso
  • Ejemplos: Bibliotecas multimedia, portfolios, presentaciones
  • Ventaja: Control total del proceso

Archivos Grandes (> 100MB)

  • Tecnología: Streams modernos
  • Ejemplos: Videos 4K, datasets, backups
  • Ventaja: Eficiencia y escalabilidad

Conclusión

En 2025, la subida de archivos ha evolucionado hacia soluciones más elegantes y eficientes. Los streams modernos representan el futuro, ofreciendo mejor rendimiento con menos código.

La progresión ideal:

  1. Comienza con FormData para validar funcionalidad básica
  2. Migra a XMLHttpRequest cuando necesites UX profesional
  3. Adopta Streams para archivos grandes y máximo rendimiento

La clave está en elegir la herramienta correcta para cada situación específica, priorizando siempre la experiencia del usuario y la mantenibilidad del código.


¿Necesitas Ayuda Implementando Estas Soluciones?

Si tienes una aplicación web que necesita un sistema robusto de carga de archivos, nuestro equipo puede ayudarte. Implementamos desde soluciones básicas con FormData hasta sistemas enterprise con streams modernos y optimización de rendimiento.

Servicios que ofrecemos:

  • ✅ Implementación completa de sistemas de upload
  • ✅ Optimización de rendimiento para archivos grandes
  • ✅ Integración con servicios cloud (AWS S3, Google Cloud, Azure)
  • ✅ Sistemas de seguridad y validación avanzados
  • ✅ APIs personalizadas y dashboards de administración

Contáctanos para una consulta gratuita →

Cuéntanos sobre tu proyecto y te ayudaremos a elegir la mejor solución para tus necesidades específicas.

meta cover

¿Qué es el Fullstack Data Flow?

Checa este otro Post

meta cover

React Hook Form es más fácil que Formik

Checa este otro Post

¡Nuevo curso!

Animaciones web con React + Motion 🧙🏻