Upload de arquivos com Multer
1. Introdução ao Multer e configuração inicial
Multer é um middleware para Node.js que facilita o upload de arquivos em aplicações Express. Ele processa requisições HTTP com multipart/form-data, o formato padrão para envio de arquivos via formulários HTML ou APIs REST. Diferente de outras soluções, o Multer oferece controle granular sobre armazenamento, validação e manipulação de arquivos.
Para começar, instale as dependências necessárias:
npm install express multer
A configuração básica do middleware no servidor Express:
const express = require('express');
const multer = require('multer');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('arquivo'), (req, res) => {
res.json({ mensagem: 'Arquivo recebido com sucesso!' });
});
app.listen(3000, () => console.log('Servidor rodando na porta 3000'));
2. Configurando o armazenamento de arquivos
diskStorage: salvando no sistema de arquivos local
O diskStorage permite controlar onde e como os arquivos são salvos no disco:
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, uniqueSuffix + '-' + file.originalname);
}
});
const upload = multer({ storage: storage });
memoryStorage: mantendo em buffer para processamento
Ideal para processar arquivos em memória sem salvar no disco (útil para redimensionamento de imagens ou envio direto para cloud):
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
app.post('/upload', upload.single('arquivo'), (req, res) => {
// req.file.buffer contém o arquivo em buffer
console.log('Tamanho do buffer:', req.file.buffer.length);
res.json({ mensagem: 'Arquivo em buffer recebido' });
});
3. Filtros de arquivo e validação
Limitando tipos de arquivo com fileFilter
const upload = multer({
storage: storage,
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Tipo de arquivo não permitido. Apenas JPEG, PNG e GIF.'), false);
}
}
});
Controlando o tamanho máximo
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
}
});
Tratamento de erros de validação
app.post('/upload', (req, res) => {
upload.single('arquivo')(req, res, (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ erro: 'Arquivo muito grande. Máximo 5MB.' });
}
return res.status(400).json({ erro: err.message });
} else if (err) {
return res.status(400).json({ erro: err.message });
}
res.json({ mensagem: 'Upload realizado com sucesso!' });
});
});
4. Upload de arquivo único vs múltiplos arquivos
single(): um arquivo por campo
app.post('/upload/single', upload.single('avatar'), (req, res) => {
res.json({ arquivo: req.file });
});
array(): múltiplos arquivos no mesmo campo
app.post('/upload/multiple', upload.array('fotos', 5), (req, res) => {
res.json({ arquivos: req.files });
});
fields(): múltiplos campos com diferentes nomes
const cpUpload = upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'galeria', maxCount: 8 }
]);
app.post('/upload/fields', cpUpload, (req, res) => {
res.json({
avatar: req.files['avatar'],
galeria: req.files['galeria']
});
});
any(): recebendo arquivos de qualquer campo
app.post('/upload/any', upload.any(), (req, res) => {
res.json({ arquivos: req.files });
});
5. Construindo a API de upload no Node.js
Rota POST completa com acesso a metadados e salvamento em banco (Prisma):
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
app.post('/api/upload', upload.single('documento'), async (req, res) => {
try {
const { originalname, filename, size, mimetype, path } = req.file;
const arquivo = await prisma.arquivo.create({
data: {
nomeOriginal: originalname,
nomeSalvo: filename,
tamanho: size,
tipo: mimetype,
caminho: path
}
});
res.json({
sucesso: true,
arquivo: {
id: arquivo.id,
nome: arquivo.nomeOriginal,
tamanho: `${(size / 1024).toFixed(2)} KB`
}
});
} catch (error) {
res.status(500).json({ erro: 'Erro ao salvar arquivo no banco' });
}
});
6. Integração com o frontend React
Componente de upload com FormData e fetch
import React, { useState } from 'react';
function UploadArquivo() {
const [arquivo, setArquivo] = useState(null);
const [progresso, setProgresso] = useState(0);
const handleUpload = async () => {
const formData = new FormData();
formData.append('arquivo', arquivo);
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const percentual = Math.round((event.loaded * 100) / event.total);
setProgresso(percentual);
}
});
xhr.addEventListener('load', () => {
alert('Upload concluído!');
setProgresso(0);
});
xhr.open('POST', 'http://localhost:3000/upload');
xhr.send(formData);
};
return (
<div>
<input type="file" onChange={(e) => setArquivo(e.target.files[0])} />
<button onClick={handleUpload}>Enviar</button>
{progresso > 0 && (
<div>
<progress value={progresso} max="100" />
<span>{progresso}%</span>
</div>
)}
</div>
);
}
Feedback visual e tratamento de erros
function UploadComFeedback() {
const [status, setStatus] = useState('idle');
const [erro, setErro] = useState('');
const handleUpload = async () => {
setStatus('enviando');
setErro('');
try {
const formData = new FormData();
formData.append('arquivo', arquivo);
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.erro || 'Erro desconhecido');
}
setStatus('sucesso');
} catch (error) {
setStatus('erro');
setErro(error.message);
}
};
return (
<div>
{status === 'sucesso' && <p className="sucesso">Upload realizado!</p>}
{status === 'erro' && <p className="erro">{erro}</p>}
{status === 'enviando' && <p>Enviando...</p>}
</div>
);
}
7. Boas práticas e segurança
Sanitização contra path traversal
const path = require('path');
const storage = multer.diskStorage({
filename: (req, file, cb) => {
// Remove caracteres perigosos do nome original
const sanitized = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
cb(null, Date.now() + '-' + sanitized);
}
});
Limpeza de arquivos temporários
const fs = require('fs');
const path = require('path');
// Limpa arquivos com mais de 24 horas
setInterval(() => {
const uploadDir = 'uploads/';
fs.readdir(uploadDir, (err, files) => {
if (err) return;
files.forEach(file => {
const filePath = path.join(uploadDir, file);
fs.stat(filePath, (err, stats) => {
if (err) return;
const now = Date.now();
const fileAge = now - stats.mtimeMs;
if (fileAge > 24 * 60 * 60 * 1000) {
fs.unlink(filePath, () => {});
}
});
});
});
}, 60 * 60 * 1000); // Executa a cada hora
Considerações de performance e escalabilidade
Para aplicações em produção, considere usar armazenamento em cloud (AWS S3, Google Cloud Storage) com bibliotecas como multer-s3 ou multer-gcs. Isso permite escalabilidade horizontal e distribuição via CDN. Para processamento assíncrono, utilize filas com Bull e Redis para tarefas como redimensionamento de imagens ou análise de vídeos.
Referências
- Documentação oficial do Multer — Repositório oficial com exemplos completos de configuração e uso do middleware.
- Multer + Express: Upload de arquivos na prática — Tutorial completo da DigitalOcean sobre upload de arquivos com Node.js e Multer.
- Upload de múltiplos arquivos com React e Node.js — Guia do freeCodeCamp sobre integração frontend React com backend Node.js para upload de arquivos.
- Segurança em upload de arquivos com Node.js — OWASP Cheat Sheet com boas práticas de segurança para upload de arquivos.
- Multer + Prisma: Salvando metadados de arquivos — Documentação do Prisma sobre como integrar upload de arquivos com banco de dados.
- Upload com barra de progresso no React — Tutorial do Dev.to sobre implementação de barra de progresso em uploads com React e Node.js.