Upload de imagem para o Azure Storage com Node

Usar o Azure Storage com Node é uma forma simples de tirar as imagens da sua API e guardá-las em um serviço feito para isso. Neste artigo, você vê como receber uma imagem em Base64 em uma API Express, enviar o arquivo para o Blob Storage e devolver a URL ao cliente.

No artigo passado, comentei sobre os motivos para fazer o upload de imagem para o Azure Storage com C#, bem como uma implementação completa. Agora, vou abordar o envio de imagens para o Azure Storage novamente, porém utilizando JavaScript (Node).

IMPORTANTE: não utilize este tutorial para enviar imagens direto do frontend. Ou seja, não deixe nada disso no seu app Angular, React, Vue ou o que quer que seja, e mantenha esse código no backend.

O motivo desse cuidado é que precisaremos da connection string do storage. Deixar isso no front é facilitar o acesso a essa informação, já que qualquer pessoa consegue ler o código que roda no navegador.

Por que usar o Azure Storage com Node

Antes do código, vale relembrar o problema. Salvar imagens no banco de dados faz o banco crescer demais, e as imagens ficam sem uma URL própria para reaproveitamento.

Salvar em disco também não resolve, porque consome I/O da máquina e não escala. Afinal, em um cenário de escalonamento horizontal, as máquinas nascem e morrem a todo momento, e os arquivos locais desaparecem com elas.

O Azure Blob Storage resolve isso de forma independente da aplicação. Cada arquivo ganha uma URL, e o serviço oferece backup, replicação e integração com CDN.

O Blob Storage também trabalha com camadas de acesso, como Hot, Cool, Cold e Archive. Assim, imagens pouco acessadas podem migrar para camadas de armazenamento mais baratas, sem mudar nada no código da API.

Além disso, o Azure Blob Storage foi otimizado para armazenar grandes volumes de dados não estruturados, como imagens e documentos. Por isso, combinar o Azure Storage com Node é uma escolha natural para APIs que recebem uploads.

Containers e blobs

No Blob Storage, você organiza os arquivos em containers, que funcionam como pastas. Cada arquivo dentro de um container é um blob.

Assim, a URL de uma imagem segue o formato https://{account}.blob.core.windows.net/{container}/{blob}. É exatamente essa URL que a nossa API vai devolver no final.

Preparando o Azure Storage com Node

Antes de escrever a API, você precisa de uma conta de armazenamento no Azure. No portal, crie um recurso do tipo Storage Account, escolha a assinatura, o grupo de recursos, a região e um nome único para a conta.

Depois que a conta estiver pronta, abra o menu Containers e crie o container que vai receber as imagens. Guarde esse nome, porque ele aparece no código no lugar de YOUR_CONTAINER.

Acesso público e tokens SAS

Por padrão, todo container novo é privado. Ou seja, a URL que a API devolve só abre no navegador com uma requisição autorizada.

Para que qualquer pessoa veja a imagem pela URL, a conta precisa permitir acesso anônimo a blobs, e o container precisa usar o nível "Blob". Contudo, a própria Microsoft recomenda não permitir acesso anônimo quando o cenário não exige.

Nesse caso, a alternativa é gerar um token SAS com permissão de leitura e prazo de validade. Para o Azure Storage com Node, a documentação recomenda o SAS de delegação de usuário, protegido por credenciais do Microsoft Entra em vez da chave da conta.

Iniciando o projeto

Para este exemplo, estou usando uma API básica criada com o Express. Se ainda não tem o Node instalado, veja antes o guia de instalação e primeiros passos com Node e npm.

Se quiser, pode iniciar o projeto com estes comandos:

npm install -g express-generator
express myapi --no-view
cd myapi
npm install

Isso gera um projeto apenas de API, com o app.js, a pasta routes e o script de inicialização em bin/www. Além disso, vamos criar um controlador (controllers/controller.js) com o seguinte método:

exports.uploadImage = async (req, res, next) => {
    // TODO
}

Para finalizar, vamos ao arquivo de rotas (routes/index.js) e criamos uma nova rota POST apontando para o controlador:

var express = require('express');
var router = express.Router();
const controller = require('../controllers/controller');

/* GET home page. */
router.get('/', function (req, res, next) {
    res.render('index', {
        title: 'Express'
    });
});

router.post('/', controller.uploadImage);

module.exports = router;

Aumentando o limite do corpo da requisição

Imagens em Base64 costumam ser grandes, já que a codificação aumenta o tamanho do arquivo em cerca de um terço. Por padrão, o parser JSON do Express aceita corpos de até 100 KB.

Então, se o seu app recebe fotos de verdade, ajuste o limite no app.js:

// Aceita corpos JSON maiores para comportar imagens em Base64
app.use(express.json({ limit: '5mb' }));

Sem esse ajuste, a API devolve um erro 413 (Payload Too Large) antes mesmo de chegar ao controlador.

Instalando os pacotes do Azure Storage com Node

Para este caso, vamos utilizar uma biblioteca do Azure (sim, já tem pronta) para facilitar. O time do Azure disponibiliza pacotes para diferentes linguagens, e se engana quem acha que é apenas para .NET.

Neste caso, vamos utilizar o pacote chamado azure-storage, que você instala com o seguinte comando:

npm install azure-storage --save

Isso é tudo que precisamos do Azure para fazer upload das nossas imagens com o Azure Storage com Node.

Além disso, para gerar nomes aleatórios eu sempre uso um GUID. Para isso, recomendo o pacote uuid:

npm install uuid --save

Uma nota sobre o pacote azure-storage

Este tutorial usa o pacote azure-storage, que era a biblioteca oficial na época. Hoje, porém, a página do pacote no npm o marca como descontinuado.

Segundo os mantenedores, o substituto para blobs é o @azure/storage-blob, disponível desde novembro de 2019. O pacote antigo ainda recebe correções críticas, mas a recomendação é migrar em projetos novos.

O raciocínio do artigo continua o mesmo nas duas bibliotecas. Por isso, mais adiante mostro também como fica o mesmo envio no SDK novo.

Referenciando os pacotes

O primeiro passo para integrar o Azure Storage com Node é importar os módulos do Azure e do UUID. Fazemos isso no topo do controlador, desta forma:

const uuid = require('uuid/v1');
const azure = require('azure-storage');

exports.uploadImage = async (req, res, next) => {
    // TODO
}

Atenção a um detalhe de versão. A importação por caminho uuid/v1 funcionava nas versões antigas do pacote, mas as versões 8 em diante removeram esse formato.

Nesse caso, use a importação nomeada: const { v1: uuid } = require('uuid');. O restante do código continua igual.

Acessando o Blob Storage

A primeira coisa que precisamos para enviar um arquivo para o Azure é a referência de um blob e de um container. Para isso, antes precisamos das credenciais de acesso, que você encontra no portal do Azure em Storage Account, na sua conta, opção Access Keys.

Nessa mesma tela, o portal exibe as chaves e a connection string da conta. Quando recebe um único parâmetro, o método createBlobService espera justamente a connection string.

Com a credencial em mãos, vamos criar o objeto que conecta o Azure Storage com Node e permite a transferência da imagem:

const uuid = require('uuid/v1');
const azure = require('azure-storage');

exports.uploadImage = async (req, res, next) => {
    const blobSvc = azure.createBlobService("YOUR_CONNECTION_STRING");
}

Em um projeto real, não deixe esse valor fixo no código. Em vez disso, leia de uma variável de ambiente, como process.env.AZURE_STORAGE_CONNECTION_STRING.

Preparando o arquivo

Nossa imagem chega no formato Base64 e vamos dividi-la em duas partes. A primeira é o tipo da imagem, e a segunda é a imagem em si.

Além disso, precisamos gerar um nome único para a imagem, para não ter problemas de conflito. Dessa forma, o código fica assim:

const uuid = require('uuid/v1');
const azure = require('azure-storage');

exports.uploadImage = async (req, res, next) => {
    const blobSvc = azure.createBlobService("YOUR_CONNECTION_STRING");

    // Gera um nome único para o arquivo
    let filename = uuid().toString() + '.jpg';

    // Obtém a imagem em Base64 do corpo da requisição
    let rawdata = req.body.image;

    // Separa o conteúdo recebido em duas partes
    let matches = rawdata.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);

    // Obtém o tipo da imagem
    let type = matches[1];

    // Obtém a imagem em si
    let buffer = new Buffer(matches[2], 'base64');
}

A expressão regular captura dois grupos. O primeiro guarda o tipo (por exemplo, image/png), e o segundo guarda o conteúdo depois de base64,.

Assim, temos tudo que o Azure Storage com Node precisa para criar o blob com o tipo correto.

Vale notar que o construtor new Buffer() está obsoleto no Node por questões de segurança. No lugar dele, use Buffer.from(matches[2], 'base64'), que produz o mesmo resultado.

Outro ponto é validar a entrada. Se o corpo não tiver o formato esperado, matches vem nulo, e a linha seguinte lança um erro.

Salvando a imagem no Azure Storage com Node

Com tudo pronto, podemos usar o método createBlockBlobFromText do pacote do Azure Storage para enviar o conteúdo ao servidor. Apesar do nome, ele aceita tanto texto quanto um buffer.

Neste caso, estou usando async/await (não esqueça do try/catch) e, caso algo dê errado, simplesmente defino o nome do arquivo como um padrão. Isso é apenas para o exemplo e precisa de melhorias no seu código, OK?

const uuid = require('uuid/v1');
const azure = require('azure-storage');

exports.uploadImage = async (req, res, next) => {
    const blobSvc = azure.createBlobService("YOUR_CONNECTION_STRING");

    // Gera um nome único para o arquivo
    let filename = uuid().toString() + '.jpg';

    // Obtém a imagem em Base64 do corpo da requisição
    let rawdata = req.body.image;

    // Separa o conteúdo recebido em duas partes
    let matches = rawdata.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);

    // Obtém o tipo da imagem
    let type = matches[1];

    // Obtém a imagem em si
    let buffer = new Buffer(matches[2], 'base64');

    // Salva a imagem
    await blobSvc.createBlockBlobFromText('YOUR_CONTAINER', filename, buffer, {
        contentType: type
    }, function (error, result, response) {
        if (error) {
            filename = 'default.png'
        }
    });
}

A opção contentType grava o tipo da imagem nos metadados do blob. Com isso, o navegador exibe a imagem ao abrir a URL, em vez de baixá-la como um arquivo genérico.

Retornando a URL da imagem do Azure Storage com Node

Se tudo deu certo, basta retornar a URL da imagem na resposta. O código abaixo mostra também a versão final do controlador.

const uuid = require('uuid/v1');
const azure = require('azure-storage');

exports.uploadImage = async (req, res, next) => {
    const blobSvc = azure.createBlobService("YOUR_CONNECTION_STRING");

    // Gera um nome único para o arquivo
    let filename = uuid().toString() + '.jpg';

    // Obtém a imagem em Base64 do corpo da requisição
    let rawdata = req.body.image;

    // Separa o conteúdo recebido em duas partes
    let matches = rawdata.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);

    // Obtém o tipo da imagem
    let type = matches[1];

    // Obtém a imagem em si
    let buffer = new Buffer(matches[2], 'base64');

    // Salva a imagem
    await blobSvc.createBlockBlobFromText('YOUR_CONTAINER', filename, buffer, {
        contentType: type
    }, function (error, result, response) {
        if (error) {
            filename = 'default.png'
        }
    });

    const fileUrl = `https://YOUR_STORAGE.blob.core.windows.net/YOUR_CONTAINER/${filename}`;
    return res.status(200).json({
        url: fileUrl
    });
}

Pronto: a API recebe a imagem, envia para o Azure Storage com Node e devolve a URL. Agora, o frontend pode salvar esse endereço junto com o restante do formulário.

Cuidados com o código do Azure Storage com Node

Existe um detalhe importante no exemplo acima. O método createBlockBlobFromText trabalha com callback e não retorna uma Promise, então o await não espera o upload terminar.

Na prática, a resposta pode sair antes do envio, e o fallback para default.png chega tarde demais. Para corrigir, envolva a chamada em uma Promise:

// Converte o método baseado em callback em uma Promise
const uploadBlob = (blobSvc, container, name, buffer, type) =>
    new Promise((resolve, reject) => {
        blobSvc.createBlockBlobFromText(container, name, buffer, { contentType: type },
            (error) => (error ? reject(error) : resolve()));
    });

try {
    await uploadBlob(blobSvc, 'YOUR_CONTAINER', filename, buffer, type);
} catch (error) {
    // Usa uma imagem padrão caso o envio falhe
    filename = 'default.png';
}

Dessa forma, o try/catch que comentei antes passa a funcionar de verdade. Além disso, a URL só volta para o cliente depois que o blob existe.

Validando a imagem antes do envio

Outro cuidado é não confiar cegamente no que chega no corpo da requisição. Antes de enviar qualquer coisa ao Azure Storage com Node, confira o formato, o tipo e o tamanho da imagem:

// Tipos de imagem aceitos pela API
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
const maxSizeInBytes = 2 * 1024 * 1024;

const matches = (req.body.image || '').match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
if (!matches) {
    return res.status(400).json({ message: 'Invalid image format' });
}

const type = matches[1];
const buffer = Buffer.from(matches[2], 'base64');

if (!allowedTypes.includes(type) || buffer.length > maxSizeInBytes) {
    return res.status(400).json({ message: 'Image type or size not allowed' });
}

Com isso, a API rejeita arquivos inesperados antes de gastar armazenamento e banda. Além disso, a extensão do arquivo pode seguir o tipo real, em vez de usar sempre .jpg.

O mesmo envio com o SDK @azure/storage-blob

Para quem começa um projeto novo, o pacote recomendado é o @azure/storage-blob. Ele já trabalha com Promises, o que simplifica o código.

const { BlobServiceClient } = require('@azure/storage-blob');

// Cria o cliente a partir da connection string
const serviceClient = BlobServiceClient.fromConnectionString(process.env.AZURE_STORAGE_CONNECTION_STRING);
const containerClient = serviceClient.getContainerClient('YOUR_CONTAINER');
const blockBlobClient = containerClient.getBlockBlobClient(filename);

// Envia o buffer e define o tipo da imagem
await blockBlobClient.uploadData(buffer, {
    blobHTTPHeaders: { blobContentType: type }
});

// A URL do blob já vem pronta no cliente
const fileUrl = blockBlobClient.url;

Repare que a URL vem direto da propriedade url, sem montar a string à mão. Além disso, o método uploadData aceita o buffer diretamente, sem precisar de um stream.

Autenticação sem senha

A documentação do pacote aponta o DefaultAzureCredential, do pacote @azure/identity, como a forma recomendada de criar o cliente. Nesse modelo, a API se autentica com uma identidade do Microsoft Entra, e nenhuma chave fica no código ou nas variáveis de ambiente:

const { DefaultAzureCredential } = require('@azure/identity');
const { BlobServiceClient } = require('@azure/storage-blob');

// Usa a identidade do ambiente (CLI local, identidade gerenciada no Azure)
const serviceClient = new BlobServiceClient(
    'https://YOUR_STORAGE.blob.core.windows.net',
    new DefaultAzureCredential()
);

Para isso funcionar, a identidade da aplicação precisa de uma função com acesso a dados, como Storage Blob Data Contributor. Dessa forma, você controla quem grava no Azure Storage com Node pelo próprio Azure RBAC.

Testando o Azure Storage com Node

Para testar, suba a API com npm start e envie uma requisição POST para a rota criada. O corpo precisa trazer a imagem completa em Base64, com o prefixo data:image:

curl -X POST http://localhost:3000/ \
  -H "Content-Type: application/json" \
  -d '{"image":"data:image/png;base64,iVBORw0KGgo..."}'

Se tudo correr bem, a resposta traz a URL do blob recém criado:

{
  "url": "https://YOUR_STORAGE.blob.core.windows.net/YOUR_CONTAINER/3f2b9c10-0000-11ee-be56-0242ac120002.jpg"
}

Em seguida, abra o portal do Azure, entre na conta de armazenamento e confira o arquivo dentro do container escolhido para o teste. Se o container for privado, lembre que a URL só abre no navegador com um token SAS.

Conclusão

Guardar imagens fora da aplicação deixa a API mais leve e pronta para escalar. Com o Azure Storage com Node, esse processo cabe em um único controlador Express.

O fluxo é sempre o mesmo: gerar um nome único, separar o tipo do conteúdo Base64, converter para buffer, enviar o blob e devolver a URL. Por fim, mantenha a connection string no backend e prefira o SDK @azure/storage-blob em projetos novos.

Também vale lembrar dos cuidados que fazem diferença em produção. Valide o tipo e o tamanho das imagens, aguarde de fato o fim do upload e decida com calma entre container público e tokens SAS.

Se você trabalha também com .NET, o artigo irmão deste mostra o mesmo fluxo em C#. Assim, você pode comparar as duas implementações e escolher a que se encaixa melhor no seu backend.

FAQ

Posso enviar imagens para o Azure Storage direto do frontend?

Não com este tutorial, porque ele exige a connection string, que ficaria exposta no navegador. Para envios do frontend, o caminho é gerar um token SAS no backend com permissões limitadas.

Qual pacote devo usar hoje para blobs no Node?

O pacote recomendado é o @azure/storage-blob, que substitui o azure-storage. O antigo ainda recebe correções críticas, mas está descontinuado.

Por que o await não espera o createBlockBlobFromText?

Porque o método usa callback e não retorna uma Promise. Para aguardar o envio, envolva a chamada em uma Promise ou use o SDK novo.

Por que separar o tipo da imagem do conteúdo Base64?

O tipo serve para definir o contentType do blob, e o conteúdo vira o buffer enviado. Sem essa separação, o prefixo data:image corrompe o arquivo.

O container precisa existir antes do envio?

Sim, o container precisa existir na conta de armazenamento. Você pode criá-lo pelo portal do Azure ou via código antes do primeiro upload.