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