2026
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
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 usaConditionExpressionpara 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.
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:
- 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. - 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.