Back

Multer NPM: Upload de Arquivos em Node.js

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:

  1. Criamos uma instância do Multer com uma pasta de destino para uploads
  2. Usamos o middleware upload.single('document') para lidar com um único upload de arquivo
  3. 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:

  1. Validar tipos de arquivo: Sempre verifique tipos MIME e extensões
  2. Limitar tamanhos de arquivo: Evite ataques DoS de uploads de arquivos grandes
  3. Verificar malware: Considere integrar verificação de vírus para arquivos carregados
  4. Usar armazenamento seguro: Não armazene arquivos em diretórios públicos
  5. Gerar nomes de arquivo aleatórios: Não use nomes de arquivo fornecidos pelo usuário diretamente
  6. Implementar autenticação: Garanta que apenas usuários autorizados possam fazer upload de arquivos
  7. 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

Listen to your bugs 🧘, with OpenReplay

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