Express

Express

Tutorial Express: Upload de archivos con Multer

Aprende a configurar Multer para subir archivos en Express con Node.js. Controla almacenamiento, validación y manejo de FormData fácilmente.

Aprende Express y certifícate

Configuración de Multer

Multer es el middleware estándar para manejar uploads de archivos en aplicaciones Express. Este paquete procesa formularios multipart/form-data y proporciona un control granular sobre cómo y dónde se almacenan los archivos subidos.

La configuración de Multer se basa en opciones de almacenamiento que determinan el destino y el nombre de los archivos. Express 5 mantiene total compatibilidad con Multer, permitiendo integrarlo de forma nativa en el pipeline de middlewares.

Instalación y configuración básica

Para comenzar, instalamos Multer como dependencia del proyecto:

npm install multer

La configuración más simple utiliza el almacenamiento en disco (diskStorage), que guarda los archivos directamente en el sistema de archivos del servidor:

import multer from 'multer';
import path from 'path';

// Configuración básica de almacenamiento
const storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'uploads/'); // Carpeta donde se guardarán los archivos
  },
  filename: function (req, file, cb) {
    // Genera un nombre único para evitar conflictos
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
  }
});

const upload = multer({ storage: storage });

Opciones de configuración avanzada

Multer ofrece múltiples opciones de configuración para controlar el comportamiento del upload:

const upload = multer({
  storage: storage,
  limits: {
    fileSize: 5 * 1024 * 1024, // Límite de 5MB por archivo
    files: 3 // Máximo 3 archivos por request
  },
  fileFilter: function (req, file, cb) {
    // Filtro para aceptar solo imágenes
    if (file.mimetype.startsWith('image/')) {
      cb(null, true);
    } else {
      cb(new Error('Solo se permiten archivos de imagen'), false);
    }
  }
});

Configuración de almacenamiento en memoria

Para casos donde necesitamos procesar archivos temporalmente sin guardarlos en disco, Multer proporciona almacenamiento en memoria:

const memoryStorage = multer.memoryStorage();

const uploadToMemory = multer({
  storage: memoryStorage,
  limits: {
    fileSize: 2 * 1024 * 1024 // 2MB máximo
  }
});

Con esta configuración, los archivos se almacenan como Buffer en la propiedad req.file.buffer, permitiendo procesarlos directamente sin crear archivos temporales.

Configuración dinámica del destino

Multer permite configurar destinos dinámicos basados en datos del request o características del archivo:

const dynamicStorage = multer.diskStorage({
  destination: function (req, file, cb) {
    // Crear carpetas por tipo de archivo
    let folder = 'uploads/';
    if (file.mimetype.startsWith('image/')) {
      folder += 'images/';
    } else if (file.mimetype.startsWith('video/')) {
      folder += 'videos/';
    } else {
      folder += 'documents/';
    }
    cb(null, folder);
  },
  filename: function (req, file, cb) {
    // Incluir timestamp y mantener extensión original
    const timestamp = new Date().toISOString().replace(/:/g, '-');
    const extension = path.extname(file.originalname);
    cb(null, `${file.fieldname}-${timestamp}${extension}`);
  }
});

Validación y filtros personalizados

La función fileFilter permite implementar validaciones complejas antes de procesar el archivo:

const customUpload = multer({
  storage: storage,
  fileFilter: function (req, file, cb) {
    // Lista de tipos MIME permitidos
    const allowedTypes = [
      'image/jpeg',
      'image/png',
      'image/gif',
      'application/pdf'
    ];
    
    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true);
    } else {
      const error = new Error(`Tipo de archivo no permitido: ${file.mimetype}`);
      error.code = 'INVALID_FILE_TYPE';
      cb(error, false);
    }
  }
});

Configuración para múltiples campos

Multer soporta configuraciones específicas para diferentes campos de archivo en el mismo formulario:

// Configuración para avatar (un solo archivo)
const avatarUpload = multer({
  storage: multer.diskStorage({
    destination: 'uploads/avatars/',
    filename: (req, file, cb) => {
      cb(null, `avatar-${req.user.id}-${Date.now()}.jpg`);
    }
  }),
  limits: { fileSize: 1024 * 1024 }, // 1MB
  fileFilter: (req, file, cb) => {
    cb(null, file.mimetype.startsWith('image/'));
  }
});

// Configuración para documentos (múltiples archivos)
const documentsUpload = multer({
  storage: multer.diskStorage({
    destination: 'uploads/documents/',
    filename: (req, file, cb) => {
      cb(null, `doc-${Date.now()}-${file.originalname}`);
    }
  }),
  limits: { 
    fileSize: 10 * 1024 * 1024, // 10MB
    files: 5 // Máximo 5 documentos
  }
});

Esta configuración modular permite aplicar diferentes restricciones y comportamientos según el contexto de uso, manteniendo la flexibilidad necesaria para diferentes tipos de uploads en la misma aplicación.

Manejo de FormData

FormData es la interfaz estándar del navegador para construir conjuntos de datos que incluyen archivos y campos de texto. En Express 5, el manejo de FormData con Multer requiere entender cómo los datos se estructuran en el request y cómo acceder a ellos correctamente.

Cuando un formulario HTML utiliza enctype="multipart/form-data", los datos se envían en un formato especial que Multer procesa y organiza en diferentes propiedades del objeto req. Esta separación permite acceder tanto a los archivos subidos como a los campos de texto de forma independiente.

Estructura de datos en el request

Multer organiza los datos del FormData en propiedades específicas del objeto request:

import express from 'express';
import multer from 'multer';

const app = express();
const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('archivo'), (req, res) => {
  // Información del archivo subido
  console.log('Archivo:', req.file);
  
  // Campos de texto del formulario
  console.log('Campos:', req.body);
  
  // Información completa del request
  console.log('Headers:', req.headers['content-type']);
});

Acceso a campos de texto

Los campos de texto del formulario se mantienen disponibles en req.body, independientemente de la presencia de archivos:

app.post('/profile', upload.single('avatar'), (req, res) => {
  // Datos del formulario de texto
  const { nombre, email, descripcion } = req.body;
  
  // Información del archivo subido
  const avatar = req.file;
  
  if (!avatar) {
    return res.status(400).json({ 
      error: 'Avatar requerido',
      receivedData: { nombre, email, descripcion }
    });
  }
  
  // Procesar datos combinados
  const perfil = {
    nombre,
    email,
    descripcion,
    avatarPath: avatar.path,
    avatarSize: avatar.size
  };
  
  res.json({ mensaje: 'Perfil actualizado', perfil });
});

Manejo de múltiples archivos con campos

Cuando el formulario incluye múltiples archivos y campos, Multer organiza los datos manteniendo la separación clara:

// Formulario con múltiples tipos de campos
app.post('/proyecto', upload.fields([
  { name: 'portada', maxCount: 1 },
  { name: 'documentos', maxCount: 5 }
]), (req, res) => {
  // Campos de texto del formulario
  const { titulo, descripcion, categoria } = req.body;
  
  // Archivos organizados por campo
  const portada = req.files['portada'] ? req.files['portada'][0] : null;
  const documentos = req.files['documentos'] || [];
  
  // Validación de datos requeridos
  if (!titulo || !portada) {
    return res.status(400).json({
      error: 'Título y portada son requeridos',
      received: { titulo, descripcion, categoria }
    });
  }
  
  // Construcción del objeto proyecto
  const proyecto = {
    titulo,
    descripcion,
    categoria,
    portada: {
      filename: portada.filename,
      path: portada.path,
      size: portada.size
    },
    documentos: documentos.map(doc => ({
      originalName: doc.originalname,
      filename: doc.filename,
      size: doc.size
    }))
  };
  
  res.json({ proyecto });
});

Validación de FormData completo

La validación integral de FormData requiere verificar tanto archivos como campos de texto:

function validarFormularioCompleto(req, res, next) {
  const { nombre, email } = req.body;
  const archivo = req.file;
  
  // Validar campos de texto
  if (!nombre || nombre.trim().length < 2) {
    return res.status(400).json({
      error: 'Nombre debe tener al menos 2 caracteres'
    });
  }
  
  if (!email || !email.includes('@')) {
    return res.status(400).json({
      error: 'Email inválido'
    });
  }
  
  // Validar archivo
  if (!archivo) {
    return res.status(400).json({
      error: 'Archivo requerido'
    });
  }
  
  // Validar tamaño y tipo
  if (archivo.size > 2 * 1024 * 1024) {
    return res.status(400).json({
      error: 'Archivo demasiado grande (máximo 2MB)'
    });
  }
  
  next();
}

app.post('/documento', upload.single('archivo'), validarFormularioCompleto, (req, res) => {
  res.json({ 
    mensaje: 'Documento procesado correctamente',
    datos: {
      usuario: req.body.nombre,
      email: req.body.email,
      archivo: req.file.filename
    }
  });
});

Procesamiento de arrays en FormData

Los campos de array en FormData requieren manejo especial, ya que pueden enviarse como múltiples campos con el mismo nombre:

app.post('/encuesta', upload.none(), (req, res) => {
  // FormData puede enviar arrays de diferentes formas
  console.log('Body completo:', req.body);
  
  // Normalizar arrays (pueden venir como string o array)
  const intereses = Array.isArray(req.body.intereses) 
    ? req.body.intereses 
    : [req.body.intereses].filter(Boolean);
  
  const respuestas = {
    nombre: req.body.nombre,
    edad: parseInt(req.body.edad),
    intereses: intereses,
    comentarios: req.body.comentarios || ''
  };
  
  res.json({ respuestas });
});

Manejo de errores específicos de FormData

Los errores relacionados con FormData requieren tratamiento específico para proporcionar feedback útil:

app.post('/upload-completo', upload.single('archivo'), (req, res) => {
  try {
    // Verificar que se recibieron datos
    if (!req.body && !req.file) {
      return res.status(400).json({
        error: 'No se recibieron datos del formulario'
      });
    }
    
    // Procesar datos recibidos
    const resultado = {
      archivo: req.file ? {
        nombre: req.file.originalname,
        tamaño: req.file.size,
        tipo: req.file.mimetype
      } : null,
      campos: Object.keys(req.body).length > 0 ? req.body : null
    };
    
    res.json({ 
      mensaje: 'FormData procesado correctamente',
      datos: resultado
    });
    
  } catch (error) {
    res.status(500).json({
      error: 'Error procesando FormData',
      detalle: error.message
    });
  }
});

// Middleware global para errores de Multer
app.use((error, req, res, next) => {
  if (error instanceof multer.MulterError) {
    if (error.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({
        error: 'Archivo demasiado grande',
        limite: '5MB máximo'
      });
    }
    if (error.code === 'LIMIT_FILE_COUNT') {
      return res.status(400).json({
        error: 'Demasiados archivos',
        limite: 'Máximo 3 archivos'
      });
    }
  }
  
  res.status(500).json({
    error: 'Error procesando el formulario',
    detalle: error.message
  });
});

Esta aproximación integral permite manejar FormData complejo manteniendo la separación clara entre archivos y datos de texto, facilitando la validación y el procesamiento posterior de la información recibida.

Aprende Express online

Otras lecciones de Express

Accede a todas las lecciones de Express y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Express y certifícate

Ejercicios de programación de Express

Evalúa tus conocimientos de esta lección Upload de archivos con Multer con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.