A web primitiva funcionava com texto simples. Você digitava um nome de usuário ou um comentário em um formulário, clicava em enviar e uma string curta viajava até o servidor via HTTP. Esse ciclo simples de requisição-resposta definia a infraestrutura. Quando as pessoas começaram a querer compartilhar fotos, documentos e vídeos, os engenheiros tiveram que descobrir como mover dados binários brutos através de um sistema construído inteiramente para texto legível.

Se você tentar inserir um JPEG em um objeto JSON, encontrará uma barreira fundamental. O JSON é um protocolo de texto. Ele espera caracteres Unicode válidos, aspas, chaves e strings devidamente escapadas. Um arquivo binário é apenas uma longa sequência de bytes, muitos dos quais não possuem representação imprimível. Ao forçar esses bytes em uma string JSON, o parser quebra, as sequências de escape corrompem o payload e a mensagem inteira torna-se ilegível do outro lado.

A codificação Base64 surgiu como a solução alternativa óbvia. Ela mapeia novamente os dados binários para um conjunto limitado de sessenta e quatro caracteres ASCII imprimíveis. Cada três bytes binários tornam-se quatro caracteres de texto. O payload agora é um JSON válido, o que significa que sobreviverá a uma viagem através de qualquer API padrão. Mas o custo é imediato. Essa re-codificação infla o tamanho do arquivo em aproximadamente trinta e três por cento. Uma imagem de três megabytes torna-se quatro megabytes no tráfego de rede. Tanto o cliente quanto o servidor consomem ciclos extras de CPU traduzindo os dados de um lado para o outro. Mais importante ainda, muitos frameworks de servidor JSON leem todo o corpo na memória antes de processá-lo. Alguns uploads grandes simultâneos podem sobrecarregar um servidor modesto, pois cada um é mantido na RAM como uma string de texto pesada antes mesmo de ser salvo no disco. O Base64 funciona em uma emergência, mas nunca foi projetado para carregar arquivos pesados em escala de produção.

A melhor resposta é o multipart/form-data. Este formato trata uma única requisição HTTP como uma coleção de partes separadas, cada uma dividida por uma string de limite (boundary) única. Uma parte pode conter um campo de texto simples. A parte seguinte pode conter uma imagem binária bruta, marcada com seus próprios cabeçalhos Content-Type e Content-Disposition. O servidor lê o fluxo de entrada sequencialmente, monitorando os marcadores de limite, e entrega cada seção ao manipulador apropriado sem nunca precisar tratar todo o payload como um único bloco de texto.

No Node.js, essa distinção é especialmente importante. O middleware express.json() sabe como processar corpos JSON, mas não lida com streams de arquivos. Para processar uploads multipart, você precisa de um parser de streaming como o Multer ou o Busboy. Essas ferramentas se conectam ao stream de requisição bruto e o leem pedaço por pedaço (chunk by chunk). O Multer, por exemplo, permite escolher se você deseja gravar os arquivos recebidos em uma pasta temporária no disco ou manter os menores na memória. Essa decisão de configuração é crucial. Se você mantiver tudo na memória e sua aplicação receber subitamente vários arquivos grandes de uma vez, seu processo pode ficar sem espaço no heap e travar. Gravar no disco troca I/O por estabilidade, mas introduz suas próprias questões sobre limpeza e segurança de caminhos.

Para uma aplicação pequena, salvar arquivos em uma pasta local como ./uploads parece natural e rápido. O arquivo cai na mesma máquina que executa seu código, e servi-lo de volta é apenas uma questão de apontar para o caminho correto. Isso funciona perfeitamente até o momento em que deixa de funcionar.

No momento em que você coloca um load balancer à frente de um segundo servidor de aplicação, o armazenamento local torna-se um bug. Um usuário faz o upload de uma foto de perfil. O load balancer roteia a requisição para o Servidor A, e o arquivo é gravado no disco do Servidor A. Mais tarde, esse usuário solicita a visualização da imagem, mas o load balancer envia a requisição para o Servidor B. O Servidor B verifica seu próprio sistema de arquivos e não encontra nada. O arquivo está efetivamente faltando. Você pode implementar sticky sessions para fixar um usuário na mesma máquina, mas essa é uma correção frágil. Se o Servidor A reiniciar, for implantado novamente ou for substituído por uma instância de autoscaling, os dados desaparecem. Em ambientes conteinerizados, os discos locais são ainda mais efêmeros. O sistema de arquivos de um container Docker foi feito para ser descartável. Tratá-lo como armazenamento permanente é uma maneira garantida de perder os dados do usuário.

A solução padrão é separar o processamento do armazenamento. Você mantém seus servidores de aplicação sem estado (stateless) e envia os arquivos carregados para um armazenamento de objetos dedicado, como AWS S3 ou Google Cloud Storage. Esses serviços são construídos para durabilidade, distribuição geográfica e concorrência massiva. O servidor de aplicação lida com a requisição, valida os metadados e, em seguida, entrega os bytes para uma infraestrutura projetada especificamente para armazená-los.

No entanto, mesmo esse padrão cria um gargalo se for implementado de forma descuidada. Muitas equipes começam fazendo com que o navegador faça o upload do arquivo para o backend e, em seguida, o backend encaminha cada byte para o armazenamento de objetos. Se um usuário fizer o upload de um vídeo de quinhentos megabytes, seu servidor se torna um intermediário. Ele consome largura de banda ao puxar o arquivo e, depois, consome ainda mais largura de banda ao enviá-lo para o S3. A conexão permanece aberta durante toda a duração da transferência. Uploads lentos de usuários com condições de rede ruins podem ocupar conexões do servidor por minutos. O uso de memória permanece elevado se o servidor fizer o buffer do stream e, se você estiver usando uma hospedagem tarifada por uso, estará pagando duas vezes pela mesma transferência de dados. O escalonamento horizontal não resolve isso, pois cada servidor adicional que você adiciona ainda fica preso transportando bytes que não precisa ver.

Sistemas modernos resolvem o problema removendo o backend inteiramente do caminho dos dados. Em vez de aceitar o arquivo, o backend aceita apenas uma solicitação de permissão para o upload. O fluxo funciona assim:

  • O navegador solicita ao backend que inicie um upload, geralmente enviando apenas o nome do arquivo, o tipo de arquivo e a finalidade pretendida.
  • O backend autentica o usuário, valida a solicitação de acordo com as regras de negócio e usa um SDK para gerar uma URL pré-assinada temporária do provedor de armazenamento de objetos.
  • O backend retorna essa URL ao navegador. A URL é restrita a um bucket e uma chave específicos, válida por um curto período, como cinco ou quinze minutos, e assinada com um token que concede apenas as permissões precisas necessárias.
  • O navegador faz o upload do arquivo diretamente para o S3 ou GCS usando um PUT ou POST padrão. Os bytes viajam diretamente do dispositivo do usuário para