Fazer upload de arquivos com Uppy, XHRUpload e Express
Use o Dashboard do Uppy para selecionar arquivos e o XHRUpload para enviá-los a um endpoint multipart. Este guia traz um exemplo local completo do navegador ao servidor: um seletor de arquivos com indicadores de progresso e controles de nova tentativa, além de um receptor Express que retorna a contagem de bytes e o SHA-256 de cada upload.
Escolha o contrato de upload
O Dashboard fornece a interface; o XHRUpload envia um
POST multipart por arquivo. O receptor espera o campo file e responde com JSON após ler
o arquivo inteiro. Não há um segundo XMLHttpRequest escrito manualmente junto com o Uppy.
Esta é uma demonstração de transferência local. O receptor mantém cada arquivo na memória enquanto processa sua requisição, calcula seu hash e o descarta. Ele não salva arquivos, retorna links de download nem inspeciona seu conteúdo. Uma resposta de sucesso significa que o receptor leu os bytes, não que os armazenou de forma durável.
Crie o projeto
Use Bash no Linux, Node.js 24.15.0 e Yarn 4.12.0 disponível por meio de corepack yarn. O exemplo
também roda no Node.js 26.8.1; a verificação no navegador usa Chromium 145. O Node executa server.ts usando
a remoção nativa de tipos TypeScript.
O código do navegador passa pelo esbuild.
Cole isto em um diretório onde você queira criar uma pasta uppy-xhr-demo. Os parênteses mantêm seu
shell no diretório original. Se essa pasta já existir, a configuração para sem alterá-la;
escolha um novo local. Se a instalação falhar, pare e resolva o problema antes de continuar.
(
mkdir uppy-xhr-demo &&
cd uppy-xhr-demo &&
printf '%s\n' '{"private":true,"type":"module","packageManager":"yarn@4.12.0"}' > package.json &&
printf '%s\n' 'nodeLinker: node-modules' > .yarnrc.yml &&
touch yarn.lock &&
corepack yarn add --exact @uppy/core@6.0.2 @uppy/dashboard@6.0.0 @uppy/xhr-upload@6.0.0 express@5.2.1 multer@2.4.0 esbuild@0.27.0 &&
mkdir public
)
O arquivo de lock vazio torna este um projeto Yarn separado, mesmo dentro de outro projeto. Mantenha o
yarn.lock resultante para instalações reproduzíveis da combinação testada. O package.json local
estabelece o uso de módulos ES mesmo dentro de um projeto pai CommonJS;
o vinculador node-modules do Yarn permite que o Node, sem ferramentas adicionais,
resolva as importações do servidor.
Salve os três arquivos a seguir dentro de uppy-xhr-demo.
Adicione o receptor
Salve isto como server.ts. O Multer analisa o
corpo multipart e aplica limites à requisição durante o recebimento. O receptor aceita um arquivo por
requisição, de até 2 MiB, sem campos de texto extras. A verificação MIME confere apenas a declaração
de quem envia; ela não comprova que os bytes formam uma imagem ou um PDF válido.
import { createHash } from 'node:crypto'
import { join } from 'node:path'
import express from 'express'
import multer from 'multer'
const app = express()
const receive = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 2 * 1024 * 1024, files: 1, fields: 0 },
}).single('file')
const allowedTypes = new Set(['image/jpeg', 'image/png', 'application/pdf'])
app.use(express.static(join(import.meta.dirname, 'public')))
app.post('/upload', (req, res) => {
receive(req, res, (error: unknown) => {
if (error) {
const tooLarge = error instanceof multer.MulterError && error.code === 'LIMIT_FILE_SIZE'
res.status(tooLarge ? 413 : 400).json({ error: 'Upload rejected.' })
return
}
const file = req.file
if (!file) {
res.status(400).json({ error: 'Expected one file in the file field.' })
return
}
if (!allowedTypes.has(file.mimetype)) {
res.status(415).json({ error: 'Expected a JPEG, PNG, or PDF MIME type.' })
return
}
res.json({
bytes: file.size,
sha256: createHash('sha256').update(file.buffer).digest('hex'),
})
})
})
const port = Number(process.env.PORT ?? 0)
if (!Number.isInteger(port) || port < 0 || port > 65535) {
throw new Error('PORT must be an integer from 0 to 65535.')
}
const server = app.listen(port, '127.0.0.1', (error?: Error) => {
if (error) {
console.error(`Cannot start the upload server: ${error.message}`)
process.exitCode = 1
return
}
const address = server.address()
if (!address || typeof address === 'string') throw new Error('Missing listening address.')
console.log(`Open http://127.0.0.1:${address.port}`)
})
A porta zero solicita uma porta disponível ao sistema operacional. Você pode definir PORT como uma porta fixa,
se necessário. O callback trata erros de inicialização do Express 5,
portanto, uma porta ocupada faz o processo terminar com erro, em vez de imprimir um endereço que
indicaria, de forma enganosa, que o servidor está pronto.
Adicione a página e o componente de upload
Salve isto como public/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Uppy upload demo</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="/app.css" />
<style>
body { margin: 1rem; font-family: sans-serif; }
#receipts { overflow-wrap: anywhere; }
</style>
<script type="module" src="/app.js"></script>
</head>
<body>
<h1>Upload to the local receiver</h1>
<div id="drag-drop-area"></div>
<h2>Received by the server</h2>
<p>These receipts confirm transfer. Files are not saved.</p>
<ul id="receipts" aria-label="Server receipts" aria-live="polite"></ul>
</body>
</html>
Salve isto como client.ts. O Dashboard já exibe
erros de seleção, progresso do upload e controles de cancelamento e nova tentativa. O pequeno
manipulador de eventos adiciona comprovantes do servidor usando textContent, para que o nome do arquivo seja exibido como texto.
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/css/style.min.css'
import '@uppy/dashboard/css/style.min.css'
const receipts = document.querySelector('#receipts')
if (!(receipts instanceof HTMLUListElement)) throw new Error('Missing receipt list.')
const uppy = new Uppy({
autoProceed: false,
allowMultipleUploadBatches: false,
restrictions: {
maxFileSize: 2 * 1024 * 1024,
maxNumberOfFiles: 5,
allowedFileTypes: ['image/jpeg', 'image/png', '.pdf'],
},
})
.use(Dashboard, {
inline: true,
target: '#drag-drop-area',
note: 'Up to five JPEG, PNG, or PDF files, each up to 2 MiB.',
})
.use(XHRUpload, {
endpoint: '/upload',
fieldName: 'file',
formData: true,
bundle: false,
allowedMetaFields: false,
limit: 2,
shouldRetry: () => false,
onAfterResponse(xhr) {
if (xhr.status === 413) throw new Error('Choose a file no larger than 2 MiB.')
if (xhr.status === 415) throw new Error('The server requires a JPEG, PNG, or PDF MIME type.')
if (xhr.status < 200 || xhr.status >= 300) {
throw new Error('The server rejected the upload. Check the endpoint before retrying.')
}
},
})
uppy.on('upload-success', (file, response) => {
if (!file) return
const item = document.createElement('li')
item.textContent = `${file.name}: ${JSON.stringify(response.body)}`
receipts.append(item)
})
fieldName corresponde a single('file') no receptor. allowedMetaFields: false omite os campos de metadados
padrão do Uppy para respeitar o limite de zero campos do servidor. Não defina o cabeçalho Content-Type por conta própria:
o navegador fornece o delimitador multipart. Dois arquivos podem ser transferidos simultaneamente;
os demais aguardam na fila do Uppy.
Gere o bundle e teste um upload
No mesmo diretório pai em que você executou a configuração, gere o bundle do navegador e inicie o servidor:
(
cd uppy-xhr-demo &&
corepack yarn exec esbuild client.ts --bundle --format=esm --outfile=public/app.js &&
node server.ts
)
O esbuild gera tanto public/app.js quanto public/app.css.
O CSS precisa de um link próprio no HTML,
que a página acima inclui. Gerar o bundle novamente substitui esses dois arquivos gerados. Pare o
servidor em primeiro plano com Ctrl+C antes de gerar o bundle novamente e recarregue a página após
reiniciá-lo.
Abra o endereço exato impresso pelo servidor. Escolha arquivos com
browse files e use o botão de upload do Dashboard. Para um
arquivo selecionado, o texto é Upload 1 file. Cada arquivo aceito adiciona um
comprovante em Received by the server com bytes e sha256.
Um arquivo vazio com extensão PDF é uma entrada válida para esta demonstração de transferência e
recebe bytes: 0; isso não o torna um documento PDF válido.
O exemplo aceita um lote de seleção por vez. Após o início do upload, novas seleções são bloqueadas. Tente novamente os arquivos com falha nesse lote ou recarregue a página para iniciar um novo lote após a conclusão. O cancelamento limpa a seleção para que você possa escolher novamente. Os comprovantes já exibidos permanecem até você recarregar a página.
Uma barra de progresso que chega a 100% descreve a transferência, não a aceitação pelo servidor.
Somente upload-success adiciona um comprovante. Na sua própria integração, não trate o
evento complete do Uppy como prova de que todos os arquivos tiveram sucesso:
ele também é disparado quando há arquivos com falha e fornece arrays separados de
successful e failed.
Mantenha a validação no servidor
As restrições no cliente facilitam a detecção de erros antes do envio de dados. Outro cliente pode contorná-las. Este receptor limita separadamente o tamanho da requisição e a quantidade de arquivos, mas sua verificação MIME ainda confia nos metadados fornecidos por quem faz a chamada. Ele nunca renderiza, executa nem disponibiliza de volta os bytes recebidos por upload.
Antes de adaptá-lo a um serviço público, adicione autenticação e autorização, validação de conteúdo para os formatos aceitos e armazenamento adequado à sua aplicação. Aplique também limites de requisições e de concorrência: limites pequenos por arquivo não restringem o total de memória usado por muitos clientes simultâneos. A demonstração escuta apenas no endereço de loopback local e não tem autenticação nem armazenamento persistente.
Solucione problemas comuns
Configuração de CORS
Sirva a página pelo endereço HTTP impresso, em vez de abrir index.html como um arquivo local. A página
e /upload compartilham a mesma origem, portanto este exemplo não precisa de middleware de CORS.
Se você mover a API para outra origem, configure esse servidor para permitir a origem real do seu
frontend e os cabeçalhos necessários.
O CORS controla o acesso do navegador às respostas;
ele não autentica uma requisição de upload.
Erros de rede
Este exemplo desativa as novas tentativas automáticas com shouldRetry: () => false para tornar cada tentativa
observável. Pare o servidor após carregar a página e tente fazer um upload para ver o estado de
falha do Dashboard. Reinicie-o na mesma porta ou recarregue a página no novo endereço e selecione os
arquivos novamente.
Para uma requisição com falha no mesmo endpoint, corrija a causa e escolha Retry. O XHRUpload envia esse arquivo novamente desde o início. Uma resposta perdida pode deixar o navegador sem saber o resultado, mesmo que o servidor tenha recebido os bytes. Um serviço de upload com armazenamento persistente precisa de sua própria política para lidar com duplicatas.
Use a limitação de velocidade de rede do navegador com um arquivo maior, dentro do limite permitido, para testar o controle Cancel da barra de status enquanto houver trabalho pendente. O cancelamento aborta as requisições pendentes do navegador e limpa a seleção; ele não pode desfazer o recebimento de bytes que o servidor já recebeu. Isso não é uma operação de pausa e retomada.
Validação do tipo de arquivo
Um arquivo maior que o limite do cliente, uma extensão ou um tipo não compatível, ou um sexto arquivo
provoca um erro de seleção no Dashboard sem iniciar o upload desse arquivo. A regra .pdf permite a extensão
do nome do arquivo; ela não inspeciona o conteúdo do PDF. HTTP 415 significa que o receptor rejeitou
o tipo MIME declarado. HTTP 413 significa que o limite de tamanho foi excedido. Inspecione o POST no
painel de rede do navegador; alterar as configurações de CORS não corrigirá essas respostas.
Use tus quando os uploads precisarem ser retomados
O XHRUpload é uma boa opção para endpoints multipart comuns e arquivos pequenos. Alterar seu limite de concorrência não adiciona divisão em blocos nem capacidade de retomada. Para uploads que precisam continuar após uma interrupção, use o plugin Tus do Uppy com um servidor compatível com tus. Esse é um protocolo diferente e exige a substituição deste receptor multipart.
