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.
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)
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.
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.
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
eRowKey
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 comprocess.env.API_KEY
. - Validamos o corpo JSON e extraímos
url
,expiryDays
emaxVisits
. - 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 emlocal.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
elastIP
. Para obter o IP do usuário, usamos o headerX-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 paraentity.originalUrl
, realizando o redirecionamento.
Referências: A configuração da rota dinâmica
{code}
nofunction.json
permite capturar o valor na rota (Azure Functions HTTP trigger | Microsoft Learn). Para obter o IP do cliente, usamosX-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
- Configuração de funções HTTP em Azure Functions (function.json) (Azure Functions HTTP trigger | Microsoft Learn).
- Uso de parâmetros de rota (
{id}
) em Azure Functions (Azure Functions HTTP trigger | Microsoft Learn). - Chaves PartitionKey/RowKey no Azure Table Storage (Understanding the Table service data model (REST API) - Azure Storage | Microsoft Learn).
- Obtenção do IP do cliente via header
X-Forwarded-For
(Getting Client-IP from Azure Functions - .NET Isolated - Microsoft Q&A). - Uso de variáveis de ambiente em Azure Functions (local.settings.json) (App settings reference for Azure Functions | Microsoft Learn).