Upload de Arquivos no Node.js com Multer: Guia Completo Sem Armadilhas
← Voltar para Codeshort

Upload de Arquivos no Node.js com Multer: Guia Completo Sem Armadilhas

Configure upload de arquivos no Node.js com Multer, evite os erros que quebram em produção e escolha entre disco, memória ou storage externo.

DC
Dev Code Software
03 de julho de 2026·6 min de leitura

Seu endpoint de upload funciona perfeitamente no Postman. Você testa com uma imagem de 200KB, recebe 200, comemora e faz o merge do PR. Três semanas depois, um usuário sobe uma foto de 12MB direto do celular e o servidor trava. Bem-vindo ao mundo do upload de arquivos em Node.js, onde o express.json() não te salva e você precisa do Multer.

Esse post cobre o básico, mas principalmente o que não é básico: os limites, os filtros e os erros que só aparecem quando alguém real usa sua aplicação.

Índice

O que é o Multer e por que ele existe

O Express não faz parsing de multipart/form-data sozinho. Ele resolve JSON e URL-encoded com express.json() e express.urlencoded(), mas quando o corpo da requisição é um formulário com arquivo, esses middlewares simplesmente ignoram o conteúdo. É aí que entra o Multer: ele lê o stream multipart, separa os campos de texto dos arquivos e te entrega tudo organizado em req.body e req.file (ou req.files).

Por que isso importa? Porque tentar fazer esse parsing na mão é reinventar uma roda cheia de casos extremos — boundary malformado, encoding de nome de arquivo, streams que não fecham. O Multer resolve isso com uma API relativamente pequena, mas com detalhes que fazem toda diferença na hora de colocar em produção.

Instalando e configurando o básico

Instalação padrão:

npm install multer

Configuração mínima para receber um único arquivo:

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

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

app.post('/upload', upload.single('avatar'), (req, res) => {
  console.log(req.file); // metadados do arquivo
  console.log(req.body); // outros campos do form
  res.json({ ok: true, filename: req.file.filename });
});

O 'avatar' precisa bater exatamente com o name do campo no formulário ou no FormData do frontend. Esse é o primeiro erro que todo mundo comete: campo não bate, req.file vem undefined, e a mensagem de erro do Express não ajuda muito.

💡 Dica: se req.file estiver undefined e você tem certeza que o form está certo, confira se o Content-Type da requisição é mesmo multipart/form-data. Ferramentas de teste às vezes mandam como JSON por padrão.

Storage: disco vs memória

O Multer tem duas engines de storage prontas, e escolher a errada custa caro depois.

DiskStorage grava o arquivo direto no disco do servidor:

const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, 'uploads/'),
  filename: (req, file, cb) => {
    const suffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
    cb(null, `${suffix}-${file.originalname}`);
  }
});

const upload = multer({ storage });

MemoryStorage mantém o arquivo como Buffer em memória, sem tocar o disco:

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

Memória é a escolha certa quando você vai processar o arquivo antes de persistir — redimensionar imagem, extrair texto de PDF, ou subir direto para um bucket S3. Disco faz sentido quando o próprio filesystem do servidor é o destino final, ou como etapa intermediária antes de mover o arquivo para outro lugar.

❌ O que não fazer: usar memoryStorage para uploads grandes sem limite de tamanho. Cada requisição concorrente vai empilhar buffers na RAM, e não existe backpressure te protegendo disso.

✓ O que fazer: se for usar memória, sempre combine com limits.fileSize (próxima seção) e pense no volume de requisições simultâneas que seu servidor aguenta.

Filtros de arquivo e limite de tamanho

Sem isso, qualquer pessoa pode subir um executável de 500MB disfarçado de "foto de perfil".

const upload = multer({
  storage,
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB
    files: 3
  },
  fileFilter: (req, file, cb) => {
    const tiposPermitidos = ['image/jpeg', 'image/png', 'image/webp'];
    if (!tiposPermitidos.includes(file.mimetype)) {
      return cb(new Error('Tipo de arquivo não permitido'));
    }
    cb(null, true);
  }
});

O fileFilter roda antes do arquivo ser totalmente salvo, então ele é sua primeira linha de defesa. Mas atenção: file.mimetype vem do header enviado pelo cliente, e isso é facilmente falsificável. Para validação séria, combine com uma checagem do "magic number" do arquivo depois do upload — bibliotecas como file-type leem os primeiros bytes reais do arquivo em vez de confiar no que o cliente diz que é.

Erros comuns em produção

Isso apareceu num PR meu em 2023: subimos uma feature de upload de avatar sem limits.fileSize configurado, passou no code review, passou nos testes com imagens pequenas. Duas semanas depois em produção, um usuário tentou subir um vídeo de 300MB pensando que era o campo certo. O processo Node ficou preso processando o buffer e derrubou outras requisições na mesma instância. O revisor comentou só isso: "cadê o limite de tamanho?". Boa pergunta, e cara de mais uma hora de post-mortem.

Os erros que mais se repetem:

1. Não tratar MulterError separadamente:

// ❌ erro genérico sem contexto
app.post('/upload', upload.single('file'), (req, res) => {
  res.json({ ok: true });
});

// ✓ tratamento correto do erro
app.post('/upload', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      return res.status(400).json({ error: err.code });
    } else if (err) {
      return res.status(500).json({ error: 'Erro desconhecido' });
    }
    res.json({ ok: true, filename: req.file.filename });
  });
});

2. Confiar no originalname sem sanitizar. Nomes de arquivo vindos do cliente podem conter ../ e tentar path traversal, ou caracteres que quebram seu filesystem. Sempre gere um nome novo no servidor, como fizemos no exemplo de diskStorage acima.

3. Esquecer de limpar arquivos órfãos. Se a validação de negócio falhar depois que o Multer já salvou o arquivo em disco, você precisa deletar esse arquivo manualmente com fs.unlink. Senão o diretório de uploads vira um cemitério de arquivos que ninguém referencia.

⚠️ Atenção: o middleware do Multer roda antes da sua validação de negócio. Isso significa que o arquivo já está salvo em disco (ou em memória) quando você descobre que, por exemplo, o usuário não tinha permissão para fazer aquele upload. Planeje a limpeza desde o início, não como um patch depois.

Upload múltiplo e campos mistos

Para múltiplos arquivos do mesmo campo, use array:

app.post('/galeria', upload.array('fotos', 10), (req, res) => {
  res.json({ total: req.files.length });
});

Quando o form mistura campos diferentes — capa e galeria, por exemplo — fields resolve:

const camposUpload = upload.fields([
  { name: 'capa', maxCount: 1 },
  { name: 'galeria', maxCount: 8 }
]);

app.post('/produto', camposUpload, (req, res) => {
  console.log(req.files.capa);
  console.log(req.files.galeria);
  res.json({ ok: true });
});

Repare que req.files muda de formato dependendo do método: array simples com .array(), objeto indexado por nome de campo com .fields(). Confundir os dois é fonte constante de bug silencioso — o código não quebra, só lê undefined onde não devia.

Integrando com storage externo

Salvar em disco local não escala em ambientes com múltiplas instâncias ou containers efêmeros — o arquivo some no próximo deploy. Para produção, o padrão é usar memoryStorage e subir o buffer direto para um bucket:

const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');

const upload = multer({ storage: multer.memoryStorage() });
const s3 = new S3Client({ region: 'us-east-1' });

app.post('/upload', upload.single('arquivo'), async (req, res) => {
  const comando = new PutObjectCommand({
    Bucket: 'meu-bucket',
    Key: `uploads/${Date.now()}-${req.file.originalname}`,
    Body: req.file.buffer,
    ContentType: req.file.mimetype
  });

  await s3.send(comando);
  res.json({ ok: true });
});

Existe também o pacote multer-s3, que integra o storage engine direto com o SDK da AWS e evita esse passo manual. Vale a pena quando o projeto já depende fortemente do ecossistema AWS; senão, o controle manual acima costuma ser mais fácil de debugar.

FAQ

Multer funciona com Fastify ou só com Express? O Multer foi feito como middleware do Express, mas existem adaptadores e implementações equivalentes para Fastify (como @fastify/multipart). Não dá para plugar o Multer original direto no Fastify sem uma camada de compatibilidade.

Por que req.file some depois de configurar CORS? Geralmente não é o CORS em si, é a ordem dos middlewares. Se algum middleware anterior já consumiu o stream da requisição (como um body-parser genérico configurado antes do Multer), o Multer não tem mais o que ler.

Dá para validar o conteúdo real do arquivo, não só a extensão? Sim, e é recomendado. Bibliotecas como file-type leem os bytes iniciais do arquivo e identificam o tipo real, independente do que o cliente declarou no mimetype ou na extensão do nome.

Qual o limite de tamanho razoável para upload de imagem de perfil? Depende do produto, mas 2-5MB cobre a grande maioria dos casos reais de foto de celular sem compressão agressiva. Ajuste combinando com validação no frontend para não depender só do backend para cortar o excesso.

Preciso me preocupar com upload malicioso mesmo validando mimetype? Sim. Mimetype e extensão são metadados que o cliente controla. Para uploads que serão servidos publicamente, considere também escanear o conteúdo (antivírus, checagem de assinatura de arquivo) antes de expor o link final.

Próximos passos

Se seu endpoint de upload ainda não tem limits.fileSize, fileFilter e tratamento de MulterError, essa é a próxima coisa a corrigir antes de qualquer feature nova. Depois disso, avalie se diskStorage ainda faz sentido para o seu ambiente de deploy ou se já é hora de migrar para memoryStorage com upload direto para um bucket. Upload de arquivo é uma das superfícies mais simples de virar incidente em produção — vale o tempo de blindar direito uma vez só.