Back

Multer NPM: Carga de archivos en Node.js

Multer NPM: Carga de archivos en Node.js

La carga de archivos es un requisito común en aplicaciones web, pero manejarlas correctamente en Node.js puede ser un desafío. Multer es un middleware para Express.js que hace que las cargas de archivos sean sencillas y eficientes. Este artículo explora cómo usar Multer para manejar cargas de archivos en tus aplicaciones Node.js.

Puntos clave

  • Multer simplifica la carga de archivos en Node.js manejando multipart/form-data
  • Configura opciones de almacenamiento según tus necesidades (disco, memoria o nube)
  • Implementa validación y filtrado adecuados para garantizar la seguridad
  • Establece límites apropiados para prevenir abusos
  • Maneja errores con elegancia para proporcionar una buena experiencia de usuario
  • Considera el almacenamiento en la nube para aplicaciones en producción

¿Qué es Multer?

Multer es un middleware de Node.js específicamente diseñado para manejar multipart/form-data, que es el formato utilizado al cargar archivos a través de formularios HTML. Construido sobre busboy, Multer procesa eficientemente las cargas de archivos y las hace accesibles en tus rutas de Express.

Las características principales de Multer incluyen:

  • Manejo de cargas de archivos individuales y múltiples
  • Opciones de almacenamiento configurables
  • Capacidades de filtrado de archivos
  • Límites de tamaño y validación
  • Integración perfecta con Express.js

Entendiendo Multipart Form Data

Antes de profundizar en Multer, es importante entender por qué el procesamiento regular de formularios no funciona para cargas de archivos.

Cuando un formulario contiene archivos, el navegador lo codifica como multipart/form-data en lugar del predeterminado application/x-www-form-urlencoded. Esta codificación permite que los datos binarios (archivos) se transmitan junto con campos de texto.

Los middleware incorporados de Express (express.json() y express.urlencoded()) no pueden procesar datos multipart, por eso necesitamos Multer.

<!-- Este formulario enviará multipart/form-data -->
<form action=""/upload"" method=""POST"" enctype=""multipart/form-data"">
  <input type=""file"" name=""document"">
  <button type=""submit"">Upload</button>
</form>

Comenzando con Multer

Instalación

Primero, instala Multer en tu proyecto Node.js:

npm install multer

Configuración básica

Aquí hay un ejemplo simple de cómo configurar Multer en una aplicación Express:

const express = require('express');
const multer = require('multer');

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

app.post('/upload', upload.single('document'), (req, res) => {
  // req.file contiene información sobre el archivo cargado
  console.log(req.file);
  
  // req.body contiene cualquier campo de texto
  console.log(req.body);
  
  res.send('File uploaded successfully');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

En este ejemplo:

  1. Creamos una instancia de Multer con una carpeta de destino para las cargas
  2. Usamos el middleware upload.single('document') para manejar una sola carga de archivo
  3. La información del archivo cargado está disponible en req.file

Manejando diferentes escenarios de carga

Multer proporciona varios métodos para manejar diferentes escenarios de carga de archivos:

Carga de un solo archivo

// Maneja un solo archivo con el nombre de campo 'profile'
app.post('/profile', upload.single('profile'), (req, res) => {
  // req.file contiene el archivo cargado
  res.json({ 
    message: 'File uploaded successfully',
    file: req.file 
  });
});

Múltiples archivos (mismo campo)

// Maneja múltiples archivos (máximo 5) con el nombre de campo 'photos'
app.post('/photos', upload.array('photos', 5), (req, res) => {
  // req.files contiene un array de archivos
  res.json({ 
    message: `${req.files.length} files uploaded successfully`,
    files: req.files 
  });
});

Múltiples archivos (diferentes campos)

const uploadFields = upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'gallery', maxCount: 3 }
]);

app.post('/profile', uploadFields, (req, res) => {
  // req.files es un objeto con nombres de campo como claves
  // req.files.avatar contiene el archivo de avatar
  // req.files.gallery contiene los archivos de galería
  res.json({ 
    message: 'Files uploaded successfully',
    avatar: req.files.avatar,
    gallery: req.files.gallery
  });
});

Formularios multipart solo de texto

app.post('/form-data', upload.none(), (req, res) => {
  // Solo procesa campos de texto, rechaza cualquier archivo
  res.json(req.body);
});

Opciones de almacenamiento

Multer proporciona diferentes motores de almacenamiento para controlar dónde y cómo se almacenan los archivos.

Almacenamiento en disco

Para un mayor control sobre el almacenamiento de archivos, usa diskStorage:

const storage = multer.diskStorage({
  destination: function(req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function(req, file, cb) {
    // Crea un nombre de archivo único
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, file.fieldname + '-' + uniqueSuffix + getExtension(file.originalname));
  }
});

function getExtension(filename) {
  return filename.substring(filename.lastIndexOf('.'));
}

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

Esta configuración te da control sobre:

  • Dónde se almacenan los archivos (destino)
  • Cómo se nombran (nombre de archivo)

Almacenamiento en memoria

Si necesitas procesar archivos en memoria sin guardarlos en disco:

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

app.post('/upload', upload.single('file'), (req, res) => {
  // req.file.buffer contiene los datos del archivo
  // Procesa el archivo en memoria (p.ej., cárgalo en almacenamiento en la nube)
  
  // Ejemplo: Obtén el buffer y el tipo de archivo
  const fileBuffer = req.file.buffer;
  const fileType = req.file.mimetype;
  
  res.send('File processed');
});

El almacenamiento en memoria es útil cuando:

  • Estás pasando archivos a otro servicio (como S3 o Cloudinary)
  • Necesitas procesar el archivo antes de guardarlo
  • Estás trabajando en entornos sin servidor

Filtrado de archivos

Puedes controlar qué archivos son aceptados usando la opción fileFilter:

const fileFilter = (req, file, cb) => {
  // Acepta solo archivos de imagen
  if (file.mimetype.startsWith('image/')) {
    cb(null, true);
  } else {
    cb(new Error('Only image files are allowed!'), false);
  }
};

const upload = multer({ 
  storage: multer.diskStorage({...}),
  fileFilter: fileFilter
});

Límites de tamaño y seguridad

Establecer límites es crucial para la seguridad y el rendimiento:

const upload = multer({
  storage: multer.diskStorage({...}),
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB en bytes
    files: 5 // Máximo 5 archivos por solicitud
  }
});

Otras opciones de límite incluyen:

  • fieldNameSize: Tamaño máximo del nombre del campo (predeterminado 100 bytes)
  • fieldSize: Tamaño máximo del valor del campo (predeterminado 1MB)
  • fields: Número máximo de campos que no son archivos (predeterminado Infinity)
  • parts: Número máximo de partes (campos + archivos) (predeterminado Infinity)

Manejo de errores

Los errores de Multer deben manejarse adecuadamente para proporcionar retroalimentación significativa:

app.post('/upload', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      // Ocurrió un error de Multer
      if (err.code === 'LIMIT_FILE_SIZE') {
        return res.status(400).json({ error: 'File too large' });
      }
      return res.status(400).json({ error: err.message });
    } else if (err) {
      // Ocurrió un error desconocido
      return res.status(500).json({ error: 'Server error' });
    }
    
    // Todo salió bien
    res.json({ message: 'File uploaded successfully', file: req.file });
  });
});

Ejemplo completo: Construyendo un sistema de carga de archivos

Vamos a construir un sistema completo de carga de archivos con frontend y backend:

Backend (server.js)

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const app = express();
const port = 3000;

// Servir archivos estáticos
app.use(express.static('public'));

// Configurar almacenamiento
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadDir = 'uploads';
    // Crear directorio si no existe
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir);
    }
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
    cb(null, uniqueName);
  }
});

// Configurar filtro de archivos
const fileFilter = (req, file, cb) => {
  // Aceptar imágenes y PDFs
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
  
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and PDF are allowed.'), false);
  }
};

// Configurar multer
const upload = multer({
  storage: storage,
  fileFilter: fileFilter,
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
    files: 5
  }
});

// Ruta de carga de un solo archivo
app.post('/upload/single', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      return res.status(400).json({ success: false, message: err.message });
    } else if (err) {
      return res.status(400).json({ success: false, message: err.message });
    }
    
    if (!req.file) {
      return res.status(400).json({ success: false, message: 'No file provided' });
    }
    
    res.json({
      success: true,
      message: 'File uploaded successfully',
      file: {
        filename: req.file.filename,
        originalname: req.file.originalname,
        mimetype: req.file.mimetype,
        size: req.file.size,
        path: req.file.path
      }
    });
  });
});

// Ruta de carga de múltiples archivos
app.post('/upload/multiple', (req, res) => {
  upload.array('files', 5)(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      return res.status(400).json({ success: false, message: err.message });
    } else if (err) {
      return res.status(400).json({ success: false, message: err.message });
    }
    
    if (!req.files || req.files.length === 0) {
      return res.status(400).json({ success: false, message: 'No files provided' });
    }
    
    const fileDetails = req.files.map(file => ({
      filename: file.filename,
      originalname: file.originalname,
      mimetype: file.mimetype,
      size: file.size,
      path: file.path
    }));
    
    res.json({
      success: true,
      message: `${req.files.length} files uploaded successfully`,
      files: fileDetails
    });
  });
});

// Obtener lista de archivos cargados
app.get('/files', (req, res) => {
  const uploadDir = 'uploads';
  
  fs.readdir(uploadDir, (err, files) => {
    if (err) {
      return res.status(500).json({ success: false, message: 'Error reading files directory' });
    }
    
    res.json({
      success: true,
      files: files
    });
  });
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

Frontend (public/index.html)

<!DOCTYPE html>
<html lang=""en"">
<head>
  <meta charset=""UTF-8"">
  <meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
  <title>File Upload with Multer</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    .upload-section {
      margin-bottom: 30px;
      padding: 20px;
      border: 1px solid #ddd;
      border-radius: 5px;
    }
    .file-list {
      margin-top: 20px;
    }
    .file-item {
      padding: 10px;
      margin: 5px 0;
      background-color: #f5f5f5;
      border-radius: 3px;
    }
    .error {
      color: red;
      margin-top: 10px;
    }
    .success {
      color: green;
      margin-top: 10px;
    }
    progress {
      width: 100%;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <h1>File Upload with Multer</h1>
  
  <div class=""upload-section"">
    <h2>Single File Upload</h2>
    <form id=""singleUploadForm"">
      <input type=""file"" id=""singleFile"" name=""file"" required>
      <button type=""submit"">Upload File</button>
    </form>
    <div id=""singleUploadResult""></div>
    <progress id=""singleUploadProgress"" value=""0"" max=""100"" style=""display:none;""></progress>
  </div>
  
  <div class=""upload-section"">
    <h2>Multiple File Upload</h2>
    <form id=""multipleUploadForm"">
      <input type=""file"" id=""multipleFiles"" name=""files"" multiple required>
      <button type=""submit"">Upload Files</button>
    </form>
    <div id=""multipleUploadResult""></div>
    <progress id=""multipleUploadProgress"" value=""0"" max=""100"" style=""display:none;""></progress>
  </div>
  
  <div class=""upload-section"">
    <h2>Uploaded Files</h2>
    <button id=""refreshFiles"">Refresh List</button>
    <div id=""fileList"" class=""file-list""></div>
  </div>

  <script>
    document.addEventListener('DOMContentLoaded', function() {
      // Single file upload
      const singleUploadForm = document.getElementById('singleUploadForm');
      const singleUploadResult = document.getElementById('singleUploadResult');
      const singleUploadProgress = document.getElementById('singleUploadProgress');
      
      singleUploadForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const fileInput = document.getElementById('singleFile');
        
        if (!fileInput.files.length) {
          singleUploadResult.innerHTML = '<div class=""error"">Please select a file</div>';
          return;
        }
        
        const formData = new FormData();
        formData.append('file', fileInput.files[0]);
        
        singleUploadProgress.style.display = 'block';
        singleUploadProgress.value = 0;
        singleUploadResult.innerHTML = '';
        
        const xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', function(e) {
          if (e.lengthComputable) {
            const percentComplete = (e.loaded / e.total) * 100;
            singleUploadProgress.value = percentComplete;
          }
        });
        
        xhr.onload = function() {
          if (xhr.status === 200) {
            const response = JSON.parse(xhr.responseText);
            singleUploadResult.innerHTML = `<div class=""success"">${response.message}</div>`;
            singleUploadForm.reset();
            loadFiles();
          } else {
            let errorMessage = 'Upload failed';
            try {
              const response = JSON.parse(xhr.responseText);
              errorMessage = response.message || errorMessage;
            } catch (e) {}
            singleUploadResult.innerHTML = `<div class=""error"">${errorMessage}</div>`;
          }
          singleUploadProgress.style.display = 'none';
        };
        
        xhr.onerror = function() {
          singleUploadResult.innerHTML = '<div class=""error"">Network error occurred</div>';
          singleUploadProgress.style.display = 'none';
        };
        
        xhr.open('POST', '/upload/single', true);
        xhr.send(formData);
      });
      
      // Multiple file upload
      const multipleUploadForm = document.getElementById('multipleUploadForm');
      const multipleUploadResult = document.getElementById('multipleUploadResult');
      const multipleUploadProgress = document.getElementById('multipleUploadProgress');
      
      multipleUploadForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const fileInput = document.getElementById('multipleFiles');
        
        if (!fileInput.files.length) {
          multipleUploadResult.innerHTML = '<div class=""error"">Please select at least one file</div>';
          return;
        }
        
        const formData = new FormData();
        for (let i = 0; i < fileInput.files.length; i++) {
          formData.append('files', fileInput.files[i]);
        }
        
        multipleUploadProgress.style.display = 'block';
        multipleUploadProgress.value = 0;
        multipleUploadResult.innerHTML = '';
        
        const xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', function(e) {
          if (e.lengthComputable) {
            const percentComplete = (e.loaded / e.total) * 100;
            multipleUploadProgress.value = percentComplete;
          }
        });
        
        xhr.onload = function() {
          if (xhr.status === 200) {
            const response = JSON.parse(xhr.responseText);
            multipleUploadResult.innerHTML = `<div class=""success"">${response.message}</div>`;
            multipleUploadForm.reset();
            loadFiles();
          } else {
            let errorMessage = 'Upload failed';
            try {
              const response = JSON.parse(xhr.responseText);
              errorMessage = response.message || errorMessage;
            } catch (e) {}
            multipleUploadResult.innerHTML = `<div class=""error"">${errorMessage}</div>`;
          }
          multipleUploadProgress.style.display = 'none';
        };
        
        xhr.onerror = function() {
          multipleUploadResult.innerHTML = '<div class=""error"">Network error occurred</div>';
          multipleUploadProgress.style.display = 'none';
        };
        
        xhr.open('POST', '/upload/multiple', true);
        xhr.send(formData);
      });
      
      // Load files
      const fileList = document.getElementById('fileList');
      const refreshFilesButton = document.getElementById('refreshFiles');
      
      function loadFiles() {
        fetch('/files')
          .then(response => response.json())
          .then(data => {
            if (data.success) {
              if (data.files.length === 0) {
                fileList.innerHTML = '<p>No files uploaded yet</p>';
              } else {
                let html = '';
                data.files.forEach(file => {
                  html += `<div class=""file-item"">${file}</div>`;
                });
                fileList.innerHTML = html;
              }
            } else {
              fileList.innerHTML = `<div class=""error"">${data.message}</div>`;
            }
          })
          .catch(error => {
            fileList.innerHTML = '<div class=""error"">Error loading files</div>';
          });
      }
      
      refreshFilesButton.addEventListener('click', loadFiles);
      
      // Initial load
      loadFiles();
    });
  </script>
</body>
</html>

Integración con almacenamiento en la nube

Para aplicaciones en producción, a menudo se desea almacenar archivos en la nube en lugar de en tu servidor. Aquí te mostramos cómo integrar Multer con AWS S3:

const AWS = require('aws-sdk');
const multer = require('multer');
const multerS3 = require('multer-s3');

// Configurar AWS
AWS.config.update({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION
});

const s3 = new AWS.S3();

// Configurar multer para usar S3
const upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: process.env.S3_BUCKET_NAME,
    metadata: function (req, file, cb) {
      cb(null, { fieldName: file.fieldname });
    },
    key: function (req, file, cb) {
      const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
      cb(null, uniqueName);
    }
  }),
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB
  }
});

app.post('/upload', upload.single('file'), (req, res) => {
  res.json({
    success: true,
    message: 'File uploaded to S3 successfully',
    fileLocation: req.file.location // URL de S3
  });
});

Nota: Necesitarás instalar el paquete multer-s3: npm install multer-s3 aws-sdk.

Mejores prácticas de seguridad

Al implementar cargas de archivos, sigue estas mejores prácticas de seguridad:

  1. Validar tipos de archivo: Siempre verifica los tipos MIME y extensiones
  2. Limitar tamaños de archivo: Previene ataques DoS por cargas de archivos grandes
  3. Escanear malware: Considera integrar escaneo de virus para archivos cargados
  4. Usar almacenamiento seguro: No almacenes archivos en directorios públicos
  5. Generar nombres de archivo aleatorios: No uses nombres de archivo proporcionados por el usuario directamente
  6. Implementar autenticación: Asegúrate de que solo usuarios autorizados puedan cargar archivos
  7. Establecer límites de carga: Restringe el número de archivos por solicitud y por usuario

Conclusión

Multer proporciona una solución robusta para manejar cargas de archivos en aplicaciones Node.js. Siguiendo los patrones y prácticas descritos en este artículo, puedes implementar funcionalidades de carga de archivos seguras, eficientes y amigables para el usuario. Ya sea que estés construyendo un simple cargador de imágenes o un complejo sistema de gestión de documentos, la API flexible de Multer y sus capacidades de integración lo convierten en una excelente opción para manejar multipart/form-data en tus aplicaciones Express.

Preguntas frecuentes

Para archivos grandes, considera: aumentar el límite de tamaño en la configuración de Multer, implementar cargas fragmentadas para archivos muy grandes, usar streaming para procesar archivos sin cargarlos completamente en memoria, e implementar seguimiento de progreso en el frontend.

Sí, Multer funciona bien con TypeScript. Instala las definiciones de tipos con: npm install @types/multer

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers