Multer NPM: Upload de Arquivos em Node.js

O upload de arquivos é um requisito comum em aplicações web, mas lidar com eles adequadamente em Node.js pode ser desafiador. Multer é um middleware para Express.js que torna o upload de arquivos simples e eficiente. Este artigo explora como usar o Multer para gerenciar uploads de arquivos em suas aplicações Node.js.
Principais Pontos
- Multer simplifica uploads de arquivos em Node.js ao lidar com multipart/form-data
- Configure opções de armazenamento com base em suas necessidades (disco, memória ou nuvem)
- Implemente validação e filtragem adequadas para garantir segurança
- Defina limites apropriados para evitar abusos
- Trate erros de forma elegante para proporcionar uma boa experiência ao usuário
- Considere armazenamento em nuvem para aplicações em produção
O que é o Multer?
Multer é um middleware Node.js especificamente projetado para lidar com multipart/form-data
, que é o formato usado ao fazer upload de arquivos através de formulários HTML. Construído sobre o busboy, o Multer processa eficientemente uploads de arquivos e os torna acessíveis em suas rotas Express.
Principais recursos do Multer incluem:
- Gerenciamento de uploads de arquivos únicos e múltiplos
- Opções de armazenamento configuráveis
- Capacidades de filtragem de arquivos
- Limites de tamanho e validação
- Integração perfeita com Express.js
Entendendo Multipart Form Data
Antes de mergulhar no Multer, é importante entender por que o processamento regular de formulários não funciona para uploads de arquivos.
Quando um formulário contém arquivos, o navegador o codifica como multipart/form-data
em vez do padrão application/x-www-form-urlencoded
. Esta codificação permite que dados binários (arquivos) sejam transmitidos junto com campos de texto.
Os middlewares integrados do Express (express.json()
e express.urlencoded()
) não conseguem processar dados multipart, por isso precisamos do Multer.
<!-- Este formulário enviará multipart/form-data -->
<form action=""/upload"" method=""POST"" enctype=""multipart/form-data"">
<input type=""file"" name=""document"">
<button type=""submit"">Upload</button>
</form>
Começando com o Multer
Instalação
Primeiro, instale o Multer em seu projeto Node.js:
npm install multer
Configuração Básica
Aqui está um exemplo simples de como configurar o Multer em uma aplicação 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 contém informações sobre o arquivo carregado
console.log(req.file);
// req.body contém quaisquer campos de texto
console.log(req.body);
res.send('Arquivo carregado com sucesso');
});
app.listen(3000, () => {
console.log('Servidor rodando na porta 3000');
});
Neste exemplo:
- Criamos uma instância do Multer com uma pasta de destino para uploads
- Usamos o middleware
upload.single('document')
para lidar com um único upload de arquivo - As informações do arquivo carregado estão disponíveis em
req.file
Lidando com Diferentes Cenários de Upload
O Multer fornece vários métodos para lidar com diferentes cenários de upload de arquivos:
Upload de Arquivo Único
// Lidar com um único arquivo com o nome de campo 'profile'
app.post('/profile', upload.single('profile'), (req, res) => {
// req.file contém o arquivo carregado
res.json({
message: 'Arquivo carregado com sucesso',
file: req.file
});
});
Múltiplos Arquivos (Mesmo Campo)
// Lidar com múltiplos arquivos (máximo 5) com o nome de campo 'photos'
app.post('/photos', upload.array('photos', 5), (req, res) => {
// req.files contém um array de arquivos
res.json({
message: `${req.files.length} arquivos carregados com sucesso`,
files: req.files
});
});
Múltiplos Arquivos (Campos Diferentes)
const uploadFields = upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 3 }
]);
app.post('/profile', uploadFields, (req, res) => {
// req.files é um objeto com nomes de campos como chaves
// req.files.avatar contém o arquivo de avatar
// req.files.gallery contém os arquivos da galeria
res.json({
message: 'Arquivos carregados com sucesso',
avatar: req.files.avatar,
gallery: req.files.gallery
});
});
Formulários Multipart Apenas com Texto
app.post('/form-data', upload.none(), (req, res) => {
// Processa apenas campos de texto, rejeita quaisquer arquivos
res.json(req.body);
});
Opções de Armazenamento
O Multer fornece diferentes mecanismos de armazenamento para controlar onde e como os arquivos são armazenados.
Armazenamento em Disco
Para mais controle sobre o armazenamento de arquivos, use diskStorage
:
const storage = multer.diskStorage({
destination: function(req, file, cb) {
cb(null, 'uploads/');
},
filename: function(req, file, cb) {
// Cria um nome de arquivo ú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 configuração dá a você controle sobre:
- Onde os arquivos são armazenados (destination)
- Como são nomeados (filename)
Armazenamento em Memória
Se você precisa processar arquivos na memória sem salvar em disco:
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
app.post('/upload', upload.single('file'), (req, res) => {
// req.file.buffer contém os dados do arquivo
// Processe o arquivo na memória (por exemplo, upload para armazenamento em nuvem)
// Exemplo: Obter buffer e tipo do arquivo
const fileBuffer = req.file.buffer;
const fileType = req.file.mimetype;
res.send('Arquivo processado');
});
O armazenamento em memória é útil quando:
- Você está passando arquivos para outro serviço (como S3 ou Cloudinary)
- Você precisa processar o arquivo antes de salvar
- Você está trabalhando em ambientes serverless
Filtragem de Arquivos
Você pode controlar quais arquivos são aceitos usando a opção fileFilter
:
const fileFilter = (req, file, cb) => {
// Aceita apenas arquivos de imagem
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Apenas arquivos de imagem são permitidos!'), false);
}
};
const upload = multer({
storage: multer.diskStorage({...}),
fileFilter: fileFilter
});
Limites de Tamanho e Segurança
Definir limites é crucial para segurança e desempenho:
const upload = multer({
storage: multer.diskStorage({...}),
limits: {
fileSize: 5 * 1024 * 1024, // 5MB em bytes
files: 5 // Máximo de 5 arquivos por requisição
}
});
Outras opções de limite incluem:
fieldNameSize
: Tamanho máximo do nome do campo (padrão 100 bytes)fieldSize
: Tamanho máximo do valor do campo (padrão 1MB)fields
: Número máximo de campos não-arquivo (padrão Infinity)parts
: Número máximo de partes (campos + arquivos) (padrão Infinity)
Tratamento de Erros
Os erros do Multer devem ser tratados adequadamente para fornecer feedback significativo:
app.post('/upload', (req, res) => {
upload.single('file')(req, res, (err) => {
if (err instanceof multer.MulterError) {
// Ocorreu um erro do Multer
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'Arquivo muito grande' });
}
return res.status(400).json({ error: err.message });
} else if (err) {
// Ocorreu um erro desconhecido
return res.status(500).json({ error: 'Erro no servidor' });
}
// Tudo correu bem
res.json({ message: 'Arquivo carregado com sucesso', file: req.file });
});
});
Exemplo Completo: Construindo um Sistema de Upload de Arquivos
Vamos construir um sistema completo de upload de arquivos com frontend e 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 arquivos estáticos
app.use(express.static('public'));
// Configurar armazenamento
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = 'uploads';
// Criar diretório se não existir
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 arquivos
const fileFilter = (req, file, cb) => {
// Aceitar imagens e PDFs
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Tipo de arquivo inválido. Apenas JPEG, PNG, GIF e PDF são permitidos.'), false);
}
};
// Configurar multer
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
files: 5
}
});
// Rota de upload de arquivo único
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: 'Nenhum arquivo fornecido' });
}
res.json({
success: true,
message: 'Arquivo carregado com sucesso',
file: {
filename: req.file.filename,
originalname: req.file.originalname,
mimetype: req.file.mimetype,
size: req.file.size,
path: req.file.path
}
});
});
});
// Rota de upload de múltiplos arquivos
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: 'Nenhum arquivo fornecido' });
}
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} arquivos carregados com sucesso`,
files: fileDetails
});
});
});
// Obter lista de arquivos carregados
app.get('/files', (req, res) => {
const uploadDir = 'uploads';
fs.readdir(uploadDir, (err, files) => {
if (err) {
return res.status(500).json({ success: false, message: 'Erro ao ler diretório de arquivos' });
}
res.json({
success: true,
files: files
});
});
});
app.listen(port, () => {
console.log(`Servidor rodando em 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>Upload de Arquivos com 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>Upload de Arquivos com Multer</h1>
<div class=""upload-section"">
<h2>Upload de Arquivo Único</h2>
<form id=""singleUploadForm"">
<input type=""file"" id=""singleFile"" name=""file"" required>
<button type=""submit"">Enviar Arquivo</button>
</form>
<div id=""singleUploadResult""></div>
<progress id=""singleUploadProgress"" value=""0"" max=""100"" style=""display:none;""></progress>
</div>
<div class=""upload-section"">
<h2>Upload de Múltiplos Arquivos</h2>
<form id=""multipleUploadForm"">
<input type=""file"" id=""multipleFiles"" name=""files"" multiple required>
<button type=""submit"">Enviar Arquivos</button>
</form>
<div id=""multipleUploadResult""></div>
<progress id=""multipleUploadProgress"" value=""0"" max=""100"" style=""display:none;""></progress>
</div>
<div class=""upload-section"">
<h2>Arquivos Carregados</h2>
<button id=""refreshFiles"">Atualizar Lista</button>
<div id=""fileList"" class=""file-list""></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Upload de arquivo único
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"">Por favor, selecione um arquivo</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 = 'Falha no upload';
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"">Ocorreu um erro de rede</div>';
singleUploadProgress.style.display = 'none';
};
xhr.open('POST', '/upload/single', true);
xhr.send(formData);
});
// Upload de múltiplos arquivos
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"">Por favor, selecione pelo menos um arquivo</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 = 'Falha no upload';
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"">Ocorreu um erro de rede</div>';
multipleUploadProgress.style.display = 'none';
};
xhr.open('POST', '/upload/multiple', true);
xhr.send(formData);
});
// Carregar arquivos
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>Nenhum arquivo carregado ainda</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"">Erro ao carregar arquivos</div>';
});
}
refreshFilesButton.addEventListener('click', loadFiles);
// Carregamento inicial
loadFiles();
});
</script>
</body>
</html>
Integração com Armazenamento em Nuvem
Para aplicações em produção, geralmente você deseja armazenar arquivos em armazenamento em nuvem em vez de no seu servidor. Veja como integrar o Multer com o 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: 'Arquivo carregado para o S3 com sucesso',
fileLocation: req.file.location // URL do S3
});
});
Nota: Você precisará instalar o pacote multer-s3
: npm install multer-s3 aws-sdk
.
Melhores Práticas de Segurança
Ao implementar uploads de arquivos, siga estas melhores práticas de segurança:
- Validar tipos de arquivo: Sempre verifique tipos MIME e extensões
- Limitar tamanhos de arquivo: Evite ataques DoS de uploads de arquivos grandes
- Verificar malware: Considere integrar verificação de vírus para arquivos carregados
- Usar armazenamento seguro: Não armazene arquivos em diretórios públicos
- Gerar nomes de arquivo aleatórios: Não use nomes de arquivo fornecidos pelo usuário diretamente
- Implementar autenticação: Garanta que apenas usuários autorizados possam fazer upload de arquivos
- Definir limites de upload: Restrinja o número de arquivos por requisição e por usuário
Conclusão
O Multer fornece uma solução robusta para lidar com uploads de arquivos em aplicações Node.js. Seguindo os padrões e práticas descritos neste artigo, você pode implementar funcionalidades de upload de arquivos seguras, eficientes e amigáveis ao usuário. Seja construindo um simples uploader de imagens ou um sistema complexo de gerenciamento de documentos, a API flexível do Multer e suas capacidades de integração o tornam uma excelente escolha para lidar com multipart/form-data em suas aplicações Express.
Perguntas Frequentes
Para arquivos grandes, considere: aumentar o limite de tamanho na configuração do Multer, implementar uploads em partes para arquivos muito grandes, usar streaming para processar arquivos sem carregá-los inteiramente na memória e implementar rastreamento de progresso no frontend.
Sim, o Multer funciona bem com TypeScript. Instale as definições de tipo com: npm install @types/multer