Deno + Deno KV + Deno Deploy: API Serverless com Banco de Dados Embutido
O erro que me custou uma madrugada
Em março de 2024, um cliente pediu um serviço de redirecionamento de URLs para uma campanha de marketing. Requisitos: latência abaixo de 100ms, suportar picos de 5k req/s durante lives no Instagram, e estar no ar em 48 horas. Meu instinto foi levantar um Node.js com Redis na AWS, como fiz dezenas de vezes. Mas o setup de VPC, ElastiCache, ECS, ALB e o pipeline de deploy consumiram 6 horas só de infraestrutura. Quando o serviço ficou pronto, a latência p95 batia 210ms por causa do round-trip até a região us-east-1 (o público era 90% Brasil).
Refiz tudo com Deno Deploy + Deno KV em uma tarde. O cold start caiu para 47ms, a latência p95 para 38ms (edge em GRU), e o custo mensal ficou em $0 dentro do free tier. Esse post é o que eu gostaria de ter lido antes daquela madrugada.
O que é Deno KV e por que ele muda a equação serverless
Deno KV é um banco de dados chave-valor transacional embutido no runtime Deno. Localmente, ele usa SQLite. No Deno Deploy, ele roda sobre FoundationDB, o mesmo motor que sustenta o Apple iCloud e o Snowflake. Isso significa que você tem um banco de dados com consistência forte, replicação global e zero configuração, sem instalar nada, sem connection string, sem driver.
A diferença fundamental para quem vem de Node.js: no ecossistema Node, o runtime (Node.js/libuv) cuida de I/O e você conecta bancos externos via TCP. No Deno Deploy, o banco é parte do runtime. Não existe round-trip de rede para o banco, porque o KV roda no mesmo processo de edge.
| Aspecto | Node.js + Redis (ElastiCache) | Node.js + Postgres (Neon) | Deno Deploy + Deno KV |
|---|---|---|---|
| Cold start típico | 150-400ms (Lambda) | 200-600ms (Lambda + connection) | 30-60ms |
| Latência de leitura | 1-5ms (mesma AZ) | 5-20ms (serverless proxy) | <1ms (co-located) |
| Setup de infra | VPC, Security Group, ElastiCache | Connection pooling, migrations | Zero |
| Custo até 100k req/dia | ~$15-30/mês | ~$5-10/mês | $0 (free tier) |
| Modelo de dados | Chave-valor, sem transações nativas | Relacional completo | Chave-valor com transações ACID |
| Replicação global | Manual (Global Datastore) | Read replicas manuais | Automática (35+ regiões) |
Estrutura do projeto: encurtador de URLs
Vou construir um encurtador de URLs completo para ilustrar cada peça. A estrutura é propositalmente simples: um único arquivo main.ts para a API e um kv.ts para a camada de dados. Em produção, eu separo mais, mas para um serviço focado como este, menos arquivos significa menos overhead cognitivo.
// kv.ts - Camada de acesso ao Deno KV
// Abrimos o KV sem argumentos: localmente usa SQLite em /tmp,
// no Deno Deploy conecta automaticamente ao FoundationDB distribuído.
const kv = await Deno.openKv();
export interface ShortenedUrl {
originalUrl: string;
slug: string;
clicks: number;
createdAt: string;
expiresAt: string | null;
}
// Chaves compostas permitem queries eficientes por prefixo.
// ["urls", slug] para lookup rápido no redirect.
// ["urls_by_date", isoDate, slug] para listagem cronológica.
export async function createShortUrl(
slug: string,
originalUrl: string,
ttlDays: number | null = null
): Promise<ShortenedUrl> {
const now = new Date();
const entry: ShortenedUrl = {
originalUrl,
slug,
clicks: 0,
createdAt: now.toISOString(),
expiresAt: ttlDays
? new Date(now.getTime() + ttlDays * 86_400_000).toISOString()
: null,
};
// Transação atômica: garante que o slug não foi criado entre
// o check e o set (race condition real em serverless com múltiplas instâncias)
const result = await kv.atomic()
.check({ key: ["urls", slug], versionstamp: null }) // só cria se não existir
.set(["urls", slug], entry)
.set(["urls_by_date", now.toISOString(), slug], entry)
.commit();
if (!result.ok) {
throw new Error(`Slug "${slug}" já existe`);
}
return entry;
}
O check com versionstamp: null é o equivalente a um
Leia o artigo completo em https://vivodecodigo.com.br/backend/deno-kv-deploy-api-serverless-database-javascript