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:
- Creamos una instancia de Multer con una carpeta de destino para las cargas
- Usamos el middleware
upload.single('document')
para manejar una sola carga de archivo - 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:
- Validar tipos de archivo: Siempre verifica los tipos MIME y extensiones
- Limitar tamaños de archivo: Previene ataques DoS por cargas de archivos grandes
- Escanear malware: Considera integrar escaneo de virus para archivos cargados
- Usar almacenamiento seguro: No almacenes archivos en directorios públicos
- Generar nombres de archivo aleatorios: No uses nombres de archivo proporcionados por el usuario directamente
- Implementar autenticación: Asegúrate de que solo usuarios autorizados puedan cargar archivos
- 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