Você já pensou em criar seu próprio serviço de encurtamento de URL? E que tal usar apenas serviços serverless e com custo praticamente zero?

Este projeto implementa uma API de encurtador de URLs usando Azure Functions em JavaScript, armazenando as URLs encurtadas no Azure Table Storage. Ele atende aos requisitos de: redirecionamento de URLs, expiração por tempo ou acessos, autenticação básica para criação e registro de métricas (acessos, data e IP). A seguir descrevemos a arquitetura, configuração e código completo do projeto.

Neste artigo, você vai aprender a criar um URL shortener com Azure Functions, armazenando os dados em Azure Table Storage e utilizando Identidade Gerenciada (Managed Identity) para acesso seguro sem armazenar secrets.


🏗️ Arquitetura e Estrutura do Projeto

Esta solução utiliza Azure Function App (Node.js) para encurtar e redirecionar URLs, Azure Table Storage como banco NoSQL e autenticação por API Key. O usuário faz requisições HTTP ao Function App, que valida a chave de API, gera/consulta o mapeamento de URL no Table Storage (acessado com identidade gerenciada) e registra dados de análise (IP e timestamp) antes de redirecionar. O diagrama de sequência abaixo ilustra todo o fluxo de interação (requisição de encurtamento e redirecionamento) e a coleta de analytics. As tecnologias escolhidas seguem as melhores práticas da Microsoft: o Azure Functions executa código sem servidor disparado por requisições HTTP, e o Azure Table Storage oferece armazenamento NoSQL barato e escalável para dados estruturados. Para organizar o projeto, usamos um diagrama de sequência e o modelo C4 (Contexto, Container, Componente, Código).

Diagrama de Sequência

O seguinte diagrama de sequência mostra o fluxo completo: (1) o usuário faz HTTP POST à função ShortenFunction com a URL original e a chave de API; (2) ShortenFunction valida a chave de API, gera um código curto único, calcula a expiração (por tempo ou contagem de acessos) e grava o registro em Azure Table Storage; (3) a função retorna a URL encurtada ao usuário; (4) quando o usuário acessa a URL curta, ele faz HTTP GET à função RedirectFunction; (5) RedirectFunction consulta o Table Storage, verifica se a URL expirou (tempo ou limite de acessos) e, se válido, registra o IP e data no analytics e incrementa o contador de acessos; (6) finalmente a função responde com um redirecionamento HTTP (302) para a URL original. Em cada etapa de acesso (mesmo em erros), o sistema coleta IP e timestamp para análises.

Diagrama de sequencia

No fluxo acima destacamos as etapas de autenticação (por API Key) e coleta de analytics. Como o Azure Functions usa identidades gerenciadas, a função comunica-se com o Table Storage sem expor credenciais, seguindo padrões de segurança em nuvem (Introduction to Table storage - Object storage in Azure | Microsoft Learn). A divisão do diagrama em chamadas HTTP e operações de banco espelha diretamente os componentes principais definidos na arquitetura C4 a seguir.

Diagrama C4 – Nível de Contexto

No diagrama de contexto (nível 1 do C4) mostramos o usuário externo, a Azure Function App (encurtador) e o Azure Table Storage. Segundo o modelo C4, este nível destaca entidades externas e como elas interagem com o sistema. Aqui, o usuário (pessoa ou serviço cliente) consome a API do Function App; o Function App, por sua vez, acessa o Table Storage para armazenar e recuperar URLs. O diagrama de contexto esclarece que o serviço de armazenamento é gerenciado pelo Azure e serve como banco de dados do sistema. Em uma visão de contexto, incluem-se todas as fronteiras externas ao sistema principal (por exemplo, o Table Storage)

System Context

No diagrama acima, user é o ator externo, functionApp representa o encurtador implementado em Azure Functions, e azureStorage é o serviço de Table Storage. Há uma seta do usuário para o functionApp (requisições de API) e do functionApp para o storage (operações CRUD nos dados), ilustrando o fluxo geral. Este diagrama fornece uma visão geral do sistema em seu ambiente, deixando claro o papel de cada entidade externa.

Diagrama C4 – Nível de Containers

No nível de containers (nível 2 do C4) detalhamos os principais componentes executáveis do sistema dentro do limite do Function App e do Storage. Dividimos a aplicação serverless em dois containers (funções) distintos: ShortenFunction e RedirectFunction, ambos implementados em Node.js dentro do Function App. Adicionalmente, o Azure Table Storage aparece como outro container (serviço de armazenamento). Cada função expõe um endpoint HTTP (encurtar ou redirecionar) e utiliza a autenticação por API Key (implementada via chaves de função do Azure) para proteger a operação de encurtamento. Para maior clareza, omitimos um container separado para autenticação (pois as chaves são integradas no Function App) e focamos nas funções e no Storage.

Container

Neste diagrama C4 de containers, os elementos são: ShortenFunction (função que gera URLs curtas, validando a API Key), RedirectFunction (função que processa o redirecionamento) e Azure Table Storage (serviço externo de dados). As relações mostram que o usuário invoca cada função via HTTP, e que ambas as funções interagem com o storage. O uso de dois containers separados deixa claro o padrão “Single Responsibility”: cada função faz uma tarefa (encurtar ou redirecionar). A autenticação por API Key está implícita na comunicação entre o usuário e ShortenFunction (representada na label da seta) — normalmente configurada como chave do trigger HTTP do Azure Functions.

Diagrama C4 – Nível de Componentes

No nível de componentes (nível 3 do C4) mergulhamos no interior de cada container (função) e descrevemos os módulos/libraries principais. Dentro de ShortenFunction, destacamos componentes como Autenticação (validação da API Key), Geração de Código (cria código curto único), Gerenciamento de Expiração (define TTL ou contador) e Persistência (acesso ao Table Storage). Dentro de RedirectFunction, há componentes como Consulta de Armazenamento (busca mapeamento no Table Storage). O objetivo é mostrar a responsabilidade de cada parte do código dentro das funções.

ShortenFunction

RedirectFunction

Nos diagramas acima, cada Container (ShortenFunction e RedirectFunction) tem seus Componentes internos representados. As funções de autenticação, geração de código, armazenamento e expiração dentro de ShortenFunction suportam o processo de criação da URL curta. Já em RedirectFunction, os componentes cuidam de buscar a URL original, verificar expiração, registrar analytics e redirecionar. Mostramos explicitamente, por exemplo, que storageModule/storageRF acessam o Azure Table Storage (conforme ilustrado nas relações do nível anterior). Esta visão detalha a arquitetura interna e como os módulos se relacionam, seguindo o propósito do nível de Componentes do C4.

Pseudocódigo das Funções (Visão de Código) e estrutura de arquivos

Podemos ilustrar em pseudocódigo as rotinas internas principais de cada função. Abaixo há um exemplo simplificado de como seriam as funções ShortenFunction e RedirectFunction em pseudocódigo:

// ShortenFunction: gera uma URL curta
function ShortenFunction(request) {
    validateApiKey(request.headers['x-api-key'])
    if not valid: return HttpResponse(401, "API Key inválida")
    let originalUrl = request.body.url
    let shortCode = generateUniqueCode(originalUrl)
    let expiration = computeExpiration(request.body.expiration, request.body.maxAccesses)
    // Persiste no Table Storage (via identidade gerenciada)
    storage.insert({ shortCode, originalUrl, expiration, accessCount: 0 })
    return HttpResponse(200, { shortUrl: "https://encurtador/" + shortCode })
}

// RedirectFunction: redireciona para URL original
function RedirectFunction(request) {
    let shortCode = extractCodeFromPath(request.path)
    let entry = storage.lookup(shortCode)
    // Registra dados de acesso (IP e timestamp) no analytics
    analytics.insert({ shortCode, ip: request.ip, timestamp: now() })
    if (entry == null || entry.isExpired()) {
        return HttpResponse(404, "URL expirada ou inexistente")
    }
    entry.accessCount += 1
    storage.update(entry)
    return HttpResponse(302, { Location: entry.originalUrl })
}

Este pseudocódigo resume o fluxo interno: validação da chave, geração do código único, cálculo de expiração, leitura/gravação no armazenamento, registro de analytics e envio da resposta HTTP apropriada. Apesar de simplificado, ele está alinhado com os componentes descritos (autenticação, armazenamento, expiração, analytics) e pode servir de base para uma implementação real.

A estrutura de arquivos do projeto pode ser, por exemplo:

EncurtadorURL/
├─ host.json
├─ local.settings.json
├─ ShortenFunction/
│  ├─ function.json
│  └─ index.js
└─ RedirectFunction/
   ├─ function.json
   └─ index.js
  • host.json: Configuração global do Function App (ex.: remover o prefixo /api das rotas).
  • local.settings.json: Variáveis de ambiente para desenvolvimento local (chave de conexão com Storage, API_KEY, etc.) (App settings reference for Azure Functions | Microsoft Learn).
  • ShortenFunction: função HTTP POST que recebe a URL longa e cria um código curto no Table Storage.
  • RedirectFunction: função HTTP GET que recebe o código curto na rota, busca a URL original no Table Storage, verifica expiração, incrementa estatísticas e redireciona.

🚀 Configuração Geral (host.json e local.settings.json)

No arquivo host.json desativamos o prefixo padrão /api para que as rotas sejam acessíveis diretamente na raiz. Por exemplo:

{
  "version": "2.0",
  "extensions": {
    "http": {
      "routePrefix": ""
    }
  }
}

Obs.: Isso define "routePrefix": "" dentro de "extensions", conforme recomendado para remover o prefixo /api (How to remove /api from Azure Functions URLs - Ryan Peden).

No local.settings.json (apenas para testes locais) definimos as configurações de ambiente:

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "API_KEY": "MINHA_CHAVE_SECRETA"
  }
}
  • AzureWebJobsStorage aponta para a conta do Azure Table Storage (usada pelo SDK).
  • API_KEY armazena o token de autenticação para criação de URLs.

Referências: As configurações de ambiente de uma Function App são acessíveis via process.env (App settings reference for Azure Functions | Microsoft Learn). Para rodar localmente, definimos essas variáveis em local.settings.json (App settings reference for Azure Functions | Microsoft Learn).


📦 Modelo de Dados (Azure Table Storage)

Usaremos uma tabela no Table Storage (por exemplo chamada ShortUrls) para armazenar cada URL encurtada como uma entidade. A estrutura de cada entidade (linha) pode ser descrita assim:

  • PartitionKey: string fixa (ex. "ShortUrl" ou domínio da aplicação).
  • RowKey: código curto gerado (ex. "abc123"), único dentro da partição (Understanding the Table service data model (REST API) - Azure Storage | Microsoft Learn).
  • originalUrl: string com a URL completa de destino.
  • createdAt: data/hora de criação (timestamp).
  • expirationDate: data/hora de expiração (se usado expiração por tempo).
  • maxVisits: número máximo de acessos permitidos (se usado expiração por número de acessos).
  • visitCount: contador atual de acessos.
  • lastAccess: data/hora do último acesso.
  • lastIP: IP do último usuário que acessou.

Em resumo, cada entidade JSON na tabela poderia ser algo como:

{
  "PartitionKey": "ShortUrl",
  "RowKey": "abc123",
  "originalUrl": "https://exemplo.com",
  "createdAt": "2025-05-01T10:00:00Z",
  "expirationDate": "2025-05-08T10:00:00Z",
  "maxVisits": 100,
  "visitCount": 42,
  "lastAccess": "2025-05-02T12:34:56Z",
  "lastIP": "123.45.67.89"
}

Nota: No Azure Table Storage, as propriedades PartitionKey e RowKey juntas identificam unicamente cada entidade (Understanding the Table service data model (REST API) - Azure Storage | Microsoft Learn). Usamos esses campos para buscar e atualizar as URLs encurtadas eficientemente.


✏️ Função de Criação de URL Encurtada (ShortenFunction)

A função ShortenFunction é acionada via HTTP POST (rota, por exemplo, /shorten). Ela requer um cabeçalho de autorização (x-api-key) com o token válido; caso contrário, retorna 401. Depois de validado, processa o corpo JSON com os dados: pela URL original e parâmetros de expiração opcionais. Por exemplo, o corpo pode ser:

{
  "url": "https://www.exemplo.com/longa",
  "expiryDays": 7,
  "maxVisits": 100
}

O código exemplo abaixo demonstra como gerar um código curto, montar a entidade e gravá-la no Azure Table usando o SDK @azure/data-tables.

Arquivo: ShortenFunction/function.json

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["post"]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

Este function.json configura uma função HTTP trigger (método POST) anônima (Azure Functions HTTP trigger | Microsoft Learn). Usamos authLevel: "anonymous" porque faremos validação manual via header.

Arquivo: ShortenFunction/index.js

const accountUrl = process.env.AZURE_STORAGETABLE_RESOURCEENDPOINT;
const credential = new DefaultAzureCredential();

module.exports = async function (context, req) {
    const key = req.headers["x-api-key"] || "";
    // Verifica autenticação básica via API key (salva em setting API_KEY)
    if (key !== process.env.API_KEY) {
        context.res = { status: 401, body: "Unauthorized" };
        return;
    }

    const { url, expiryDays, maxVisits } = req.body || {};
    if (!url) {
        context.res = { status: 400, body: "Falta o parâmetro url." };
        return;
    }

    // Gera um código curto simples (base36 de timestamp ou rand)
    const code = Math.random().toString(36).substr(2, 6);

    // Prepara dados da entidade
    const createdAt = new Date().toISOString();
    let expirationDate = null;
    if (expiryDays) {
        const expDate = new Date();
        expDate.setDate(expDate.getDate() + parseInt(expiryDays));
        expirationDate = expDate.toISOString();
    }

    const entity = {
        partitionKey: "ShortUrl",
        rowKey: code,
        originalUrl: url,
        createdAt,
        expirationDate: expirationDate,
        maxVisits: maxVisits || null,
        visitCount: 0,
        lastAccess: null,
        lastIP: null
    };

    try {
        // Conecta e cria a tabela se não existir
        const tableClient = new TableClient(accountUrl, "ShortUrls", credential);
        await tableClient.createTable();
        // Insere a entidade no Table Storage
        await tableClient.createEntity(entity);
    } catch (err) {
        context.res = { status: 500, body: "Erro ao acessar o storage: " + err.message };
        return;
    }

    // Retorna o código curto criado
    context.res = {
        status: 201,
        body: { shortCode: code }
    };
};

Explicação do código:

  • Obtemos o token de x-api-key e comparamos com process.env.API_KEY.
  • Validamos o corpo JSON e extraímos url, expiryDays e maxVisits.
  • Geramos um código curto (pode-se usar melhor algoritmo, aqui simplificado).
  • Calculamos a data de expiração se fornecido expiryDays.
  • Preparamos a entidade com campos conforme modelo acima.
  • Usamos o pacote @azure/data-tables para conectar ao Table Storage usando a connection string padrão (AzureWebJobsStorage) e inserimos a entidade.
  • Retornamos o código curto em formato JSON.

Referência: O arquivo function.json configura a ligação HTTP da função, conforme exemplo da documentação Azure (Azure Functions HTTP trigger | Microsoft Learn). A validação do token/API key é feita manualmente no código usando as variáveis de ambiente definidas em local.settings.json (App settings reference for Azure Functions | Microsoft Learn).


✏️ Função de Redirecionamento (RedirectFunction)

A função RedirectFunction é acionada via HTTP GET em uma rota dinâmica contendo o código curto (por exemplo, /abc123). Para isso, definimos no function.json um parâmetro de rota {code}. Usamos o host.json sem prefixo, então a chamada GET /xyz executa essa função. O trecho abaixo exemplifica a configuração da rota:

Arquivo: RedirectFunction/function.json

{
  "bindings": [
    {
      "authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get"],
      "route": "{code}"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ]
}

Aqui definimos "route": "{code}", capturando qualquer segmento de rota como parâmetro. A documentação explica que parâmetros de rota podem ser definidos em function.json ou no app (ex.: {id}) e acessados no código (Azure Functions HTTP trigger | Microsoft Learn) (Azure Functions HTTP trigger | Microsoft Learn).

Arquivo: RedirectFunction/index.js

const accountUrl = process.env.AZURE_STORAGETABLE_RESOURCEENDPOINT;
const credential = new DefaultAzureCredential();

module.exports = async function (context, req) {
    const code = context.bindingData.code; // valor capturado da rota
    if (!code) {
        context.res = { status: 400, body: "Código não fornecido." };
        return;
    }

    try {
        const tableClient = new TableClient(accountUrl, "ShortUrls", credential);
        // Tenta obter a entidade pela partitionKey e rowKey (o código)
        const entity = await tableClient.getEntity("ShortUrl", code);

        // Verifica expiração por data
        if (entity.expirationDate) {
            const now = new Date().toISOString();
            if (now > entity.expirationDate) {
                context.res = { status: 410, body: "Link expirado." };
                return;
            }
        }
        // Verifica expiração por número de acessos
        if (entity.maxVisits && entity.visitCount >= entity.maxVisits) {
            context.res = { status: 410, body: "Número de acessos excedido." };
            return;
        }

        // Atualiza contagem e último acesso
        const updatedEntity = {
            ...entity,
            visitCount: (entity.visitCount || 0) + 1,
            lastAccess: new Date().toISOString(),
            lastIP: req.headers["x-forwarded-for"] || req.headers["X-Forwarded-For"] || req.connection.remoteAddress
        };
        // Escreve atualização na tabela
        await tableClient.updateEntity(updatedEntity, "Merge");

        // Redireciona para a URL original
        context.res = {
            status: 302,
            headers: {
                Location: entity.originalUrl
            }
        };
    } catch (err) {
        // Se a entidade não existir, retorna 404
        context.res = { status: 404, body: "Código não encontrado." };
    }
};

Explicação do código:

  • Capturamos o código da rota em context.bindingData.code.
  • Buscamos a entidade na tabela usando getEntity(PartitionKey, RowKey).
  • Se não encontrada, caímos no catch e retornamos 404.
  • Se encontrada, verificamos se já expirou por data ou número de acessos. Se expirado, retornamos status 410 (Gone).
  • Se ainda válida, atualizamos visitCount, lastAccess e lastIP. Para obter o IP do usuário, usamos o header X-Forwarded-For, recomendado em Azure Functions (Getting Client-IP from Azure Functions - .NET Isolated - Microsoft Q&A).
  • Por fim, enviamos resposta HTTP com status 302 e o header Location apontando para entity.originalUrl, realizando o redirecionamento.

Referências: A configuração da rota dinâmica {code} no function.json permite capturar o valor na rota (Azure Functions HTTP trigger | Microsoft Learn). Para obter o IP do cliente, usamos X-Forwarded-For, conforme orientação da documentação do Azure Functions (Getting Client-IP from Azure Functions - .NET Isolated - Microsoft Q&A).


🧭 Estrutura da Tabela no Azure

Não há um “esquema fixo” no Azure Table Storage (é NoSQL), mas definimos as propriedades de cada entidade conforme acima. Resumindo: usamos um PartitionKey fixo (ex. "ShortUrl") e o RowKey é o código curto. Os demais campos (originalUrl, expirationDate, maxVisits, visitCount, lastAccess, lastIP) são adicionados livremente à entidade. Cada inserção/atualização é um objeto JSON com esses atributos. A documentação explica que propriedades arbitrárias podem ser usadas em tabelas armazenando os dados necessários (Understanding the Table service data model (REST API) - Azure Storage | Microsoft Learn).


🧪 Testando a API (Exemplos de Requisições)

1. Criar URL encurtada (Shorten): chame via POST incluindo o token no header. Por exemplo:

curl -X POST "https://.azurewebsites.net/shorten" \
  -H "Content-Type: application/json" \
  -H "x-api-key: MINHA_CHAVE_SECRETA" \
  -d '{
        "url": "https://www.exemplo.com/algum-caminho",
        "expiryDays": 7,
        "maxVisits": 50
      }'

Resposta esperada (HTTP 201):

{ "shortCode": "a1b2c3" }

(O código "a1b2c3" é um exemplo; o real será gerado dinamicamente.)

2. Acessar URL encurtada (Redirect): faça GET passando o código no caminho. Por exemplo:

curl -I "https://.azurewebsites.net/a1b2c3"

Resposta (HTTP 302 com header Location):

HTTP/1.1 302 Found
Location: https://www.exemplo.com/algum-caminho
...

Isso redireciona o cliente para a URL original. Nos logs/entidade do Table Storage, os campos visitCount, lastAccess e lastIP serão atualizados.

3. Casos de expiração: após atingir a data de expiração ou o número máximo de acessos, o redirecionamento não ocorre e retorna status 410, por exemplo:

HTTP/1.1 410 Gone

✅ Conclusão

Você agora tem um encurtador de URLs funcional e escalável, usando:

  • Azure Functions com cobrança por execução
  • Armazenamento Table com identidade gerenciada
  • Segurança via API Key
  • Funcionalidades como expiração e analytics

Se quiser evoluir, aqui vão ideias:

  • UI com React ou Blazor
  • Registro de analytics por IP/cidade
  • Auth com Azure AD B2C
  • Uso de CosmosDB para mais escala

💬 E você?

Já construiu algo com Azure Functions ou Table Storage? Deixe nos comentários!

Referências