← Todos os projetos

irl

Encurtador de URL serverless desenhado pra latência mínima: ~350 linhas de produção, ~2× de teste.

  • ~350linhas de produção
  • ~2×linhas de teste
  • 31testes (unit + E2E)
  • 1Lambda, sem API Gateway
Papel
Autor solo
Stack
TypeScript · Nuxt / Nitro · AWS Lambda · DynamoDB · AWS CDK · Playwright
Links
GitHub

Contexto

irl é um encurtador de URL serverless com uma tese simples: cada decisão de arquitetura deve reduzir o caminho entre o clique e o redirecionamento. O projeto também foi um laboratório de uma pergunta que me interessa mais que o produto em si: como testar serverless sem conta paga e sem mentir para si mesmo?

O problema

Três problemas se cruzam num encurtador serverless bem-feito:

  • latência: cada hop extra (API Gateway, segundo serviço, cold start desnecessário) aparece no tempo de resposta do redirect;
  • consistência de IDs curtos: com nanoid de 6 caracteres, colisões são raras mas reais — e em serverless não existe memória compartilhada para coordenar;
  • testabilidade: DynamoDB e Lambda são difíceis de rodar localmente, então é tentador testar só o “caminho feliz” e aceitar que o E2E fica para depois — que é o mesmo que não testar.

Como foi construído

  • Uma única função Lambda serve o app inteiro: o Nitro empacota SSR e as rotas de API no mesmo bundle, exposto por Lambda Function URL — sem API Gateway no meio.
  • DynamoDB on-demand com uma tabela só (id, url, createdAt). A criação usa ConditionExpression para garantir unicidade sem coordenação: se o ID já existe, a escrita falha e o serviço tenta de novo com outro ID.
  • Camadas com injeção de dependência no servidor: handler → serviço → repositório. O repositório é uma interface; trocar o cliente AWS pelo banco local é trocar um parâmetro.
  • Infraestrutura como código com CDK v2: função, URL, CORS e permissões IAM com escopo mínimo, tudo definido no mesmo repositório.
  • Ambiente reprodutível com Nix, incluindo os browsers do Playwright — qualquer máquina reproduz o ambiente inteiro.
Diagrama: navegador → Function URL → Lambda Nitro → DynamoDB

Um Lambda só serve SSR e API por trás da Function URL — sem API Gateway no caminho crítico.

Decisões e trade-offs

  • Function URL em vez de API Gateway. Menos um componente no caminho crítico e sem custo de gateway; em troca, algumas capacidades prontas (autenticação, WAF, rate limiting) ficam de fora — aceitável para o escopo, e documentado.
  • IDs gerados sem coordenação. nanoid(6) + escrita condicional + retry limitado resolve colisão sem locking distribuído: menos infraestrutura, mais código de aplicação — a troca que eu faria de novo.
  • Fakes stateful em vez de mocks. Os testes de serviço rodam contra um repositório falso com estado, que se comporta como o banco; os de repositório, contra um cliente DynamoDB falso com semântica de condição. Mock que só “espera a chamada” não encontraria os bugs que apareceram.
  • E2E com browser e banco locais. O Playwright sobe o app contra um DynamoDB Local — a mesma porta e o mesmo protocolo do serviço da AWS — em vez de simular respostas.

Qualidade e verificação

O projeto tem cerca de duas vezes mais código de teste que de produção (~675 linhas contra ~350), e os testes cobrem os caminhos de erro, não só o happy path: validação de URL (incluindo esquemas perigosos), colisão de ID, item malformado, respostas 400/404/500. O processo é disciplinado: conventional commits, hook de pré-commit rodando a suíte, versionamento e changelog automáticos.

Os testes E2E encontraram dois bugs que nenhum teste unitário pegaria:

  1. O clique antes da hidratação. O Playwright clica assim que o botão está acionável — o que em conexões lentas pode acontecer antes de o Vue hidratar a página. O clique vira um submit nativo do navegador, e o input se perde. A correção é esperar o sinal de hidratação do framework antes de interagir (waitForFunction), e o app ainda tem uma pendência honesta: desabilitar o formulário até montar.
  2. O servidor fantasma. O E2E estava configurado para reaproveitar um servidor já rodando na porta de teste. Resultado: em certas execuções ele testava um build antigo e dava verde em código que não era o atual. A correção foi a mais rígida: porta ocupada agora é erro, nunca conveniência.

Esses dois casos valem como prova: sem browser e sem banco, o teste mente para você.

Resultados

O fluxo completo funciona: encurtar com validação, redirecionar, 404 explícito para IDs desconhecidos — com a release 0.2.0 versionada e changelog gerado a partir dos commits. O que eu deixaria pronto antes de chamar de completo: redirecionamento server-side (hoje o redirect resolve no cliente, o que custa round-trips extras — contradiz a própria tese de latência do projeto), CI rodando o E2E em todo push e a página com os números de latência medidos. Pendências estão no README, à vista.

O que aprendi

Infraestrutura serverless bem desenhada é aquela cujo custo de teste local é zero: com DynamoDB Local e um comando de bootstrap, o E2E roda em qualquer máquina. E teste E2E que reaproveita estado do ambiente não é E2E — é um teste unitário otimista. Melhor falhar alto.