Cloudflare Workers + CLI: Deploy de APIs JavaScript na Edge sem Docker, sem Kubernetes

Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.
Neste artigo
O problema: sua API roda em uma região só e o deploy exige um PhD em YAML
Você tem uma API em Node.js. Ela roda em São Paulo (ou, pior, em us-east-1 porque era o default). O deploy envolve Docker, registry, Kubernetes ou algum PaaS que cobra por container idle. O cold start dói. O pipeline de CI/CD tem mais linhas de configuração do que a aplicação.
Cloudflare Workers resolve uma parte específica desse problema: APIs leves, stateless ou com estado simples (KV, Durable Objects), que rodam em mais de 300 pontos de presença, com cold start abaixo de 5ms, deploy em segundos via CLI. Não substitui um backend pesado com PostgreSQL e filas. Substitui o endpoint que valida webhook, o proxy que adiciona headers, a API de configuração que lê/grava flags, o rate limiter na borda.
Este post cobre o fluxo completo: setup do projeto, código da API, KV como storage, secrets, testes locais, deploy via Wrangler CLI e integração com CI/CD. Tudo rodável.
Wrangler CLI: o que faz e o que não faz
Wrangler é a CLI oficial da Cloudflare para Workers. Ela cria projetos, roda localmente (via Miniflare), faz deploy, gerencia secrets, namespaces KV e tail de logs em tempo real.
| Capacidade | Wrangler CLI | Docker + K8s | Serverless (AWS Lambda) |
|---|---|---|---|
| Cold start típico | < 5ms | N/A (container quente) | 100-500ms (Node.js) |
| Deploy time | 5-15 segundos | minutos | 30-60 segundos |
| Regiões | 300+ PoPs automático | você escolhe | você escolhe |
| Storage integrado | KV, R2, D1, Durable Objects | externo | DynamoDB, S3 |
| Custo em idle | zero (free tier: 100k req/dia) | container rodando | zero |
| Runtime | V8 isolates (não é Node.js) | qualquer | Node.js, Python, etc. |
| Limite de CPU time | 10ms (free) / 30s (paid) | sem limite | 15 min |
A distinção de runtime importa: Workers rodam em V8 isolates, não em Node.js. Não há fs, não há child_process, não há node:net nativo. APIs da Web Platform (fetch, Request, Response, crypto, streams) estão disponíveis. Algumas APIs Node.js têm compatibilidade parcial via flag nodejs_compat.
Setup: do zero ao primeiro request local
Instale o Wrangler globalmente ou como devDependency (prefiro devDependency para fixar versão no time):
npm init -y
npm install --save-dev wrangler
npx wrangler --versionCrie o projeto com scaffold mínimo:
npx wrangler init api-edge --type javascript
cd api-edgeO arquivo wrangler.toml gerado é o centro de configuração. Ajuste para algo funcional:
name = "api-edge"
main = "src/index.js"
compatibility_date = "2024-12-01"
# Habilita APIs Node.js parciais quando necessário
compatibility_flags = ["nodejs_compat"]
[vars]
ENVIRONMENT = "production"
# KV namespace: crie antes com `wrangler kv namespace create CACHE_KV`
[[kv_namespaces]]
binding = "CACHE_KV"
id = "SEU_NAMESPACE_ID_AQUI"A API: roteamento manual sem framework
Para APIs simples (3-10 rotas), adicionar um framework como Hono ou itty-router é válido. Para entender o que acontece por baixo, comece sem framework:
// src/index.js
// Workers usam a API de fetch da Web Platform, não http.createServer do Node.js
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const path = url.pathname;
const method = request.method;
// Roteamento explícito: sem regex, sem overhead de framework
if (method === "GET" && path === "/health") {
return Response.json({ status: "ok", region: request.cf?.colo });
}
if (method === "GET" && path === "/flags") {
return handleGetFlags(env);
}
if (method === "PUT" && path.startsWith("/flags/")) {
const flagName = path.split("/flags/")[1];
return handleSetFlag(request, env, flagName);
}
return Response.json({ error: "not found" }, { status: 404 });
},
};
async function handleGetFlags(env) {
// KV list retorna chaves com prefixo: útil para agrupar dados por domínio
const list = await env.CACHE_KV.list({ prefix: "flag:" });
const flags = {};
for (const key of list.keys) {
const value = await env.CACHE_KV.get(key.name, "json");
flags[key.name.replace("flag:", "")] = value;
}
return Response.json(flags);
}
async function handleSetFlag(request, env, flagName) {
if (!flagName || flagName.length > 64) {
return Response.json({ error: "flag name inválido" }, { status: 400 });
}
const body = await request.json().catch(() => null);
if (!body || typeof body.enabled !== "boolean") {
return Response.json(
{ error: "body deve conter { enabled: boolean }" },
{ status: 400 }
);
}
// expirationTtl em segundos: flags expiram em 24h para evitar lixo no KV
await env.CACHE_KV.put(`flag:${flagName}`, JSON.stringify(body), {
expirationTtl: 86400,
});
return Response.json({ flagName, ...body }, { status: 200 });
}Rode localmente:
npx wrangler dev
# Escuta em http://localhost:8787
# Miniflare simula KV, secrets e bindings localmenteTeste com curl:
curl http://localhost:8787/health
# {"status":"ok","region":null}
curl -X PUT http://localhost:8787/flags/dark-mode \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
# {"flagName":"dark-mode","enabled":true}
curl http://localhost:8787/flags
# {"dark-mode":{"enabled":true}}Secrets e variáveis: o que vai no TOML e o que não vai
Variáveis públicas (environment, feature flags estáticos) vão em [vars] no wrangler.toml. Secrets (API keys, tokens) vão via CLI:
# Secret fica encriptado no painel da Cloudflare, nunca no repositório
npx wrangler secret put API_TOKEN
# O CLI pede o valor interativamente
# Para listar secrets existentes
npx wrangler secret listNo código, secrets aparecem no objeto env da mesma forma que variáveis:
// src/middleware/auth.js
// Autenticação simples via Bearer token comparando com secret
export function authenticateRequest(request, env) {
const authHeader = request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return { authenticated: false, error: "Missing Authorization header" };
}
const token = authHeader.slice(7);
// Comparação timing-safe: crypto.subtle está disponível em Workers
// Para tokens curtos, timingSafeEqual evita side-channel attacks
const encoder = new TextEncoder();
const tokenBytes = encoder.encode(token);
const secretBytes = encoder.encode(env.API_TOKEN);
if (tokenBytes.byteLength !== secretBytes.byteLength) {
return { authenticated: false, error: "Invalid token" };
}
const isValid = crypto.subtle.timingSafeEqual(tokenBytes, secretBytes);
return { authenticated: isValid, error: isValid ? null : "Invalid token" };
}O que NÃO fazer
Erro 1: usar KV como banco de dados transacional.
KV é eventually consistent. Escritas propagam para todas as regiões em até 60 segundos. Se você escreve em São Paulo e lê em Frankfurt 500ms depois, pode ler o valor antigo.
// ERRADO: tratar KV como fonte de verdade para contagem
async function incrementCounter(env) {
const current = await env.CACHE_KV.get("counter", "text");
const next = (parseInt(current || "0", 10) + 1).toString();
// Race condition: duas requests simultâneas leem o mesmo valor
await env.CACHE_KV.put("counter", next);
return next;
}// CORRETO: usar Durable Objects para estado consistente,
// ou aceitar que KV é cache e tratar contagem como aproximação
// Para contagem exata, use Durable Objects ou um banco externo
// Se KV é suficiente (analytics aproximados), documente a limitação
async function incrementCounterApproximate(env) {
const current = await env.CACHE_KV.get("counter", "text");
const next = (parseInt(current || "0", 10) + 1).toString();
await env.CACHE_KV.put("counter", next);
// Retorna valor aproximado: duas requests no mesmo PoP podem colidir
return next;
}Erro 2: ignorar o limite de CPU time.
Workers no plano free têm 10ms de CPU time por request. Isso não é wall time (I/O não conta). Mas parsing de JSON grande, criptografia pesada ou loops intensivos estouram fácil.
// ERRADO: processar CSV de 10MB dentro do Worker
async function handleUpload(request) {
const text = await request.text();
const rows = text.split("\n").map((row) => {
// Parsing linha a linha com split e map: CPU-bound
const cols = row.split(",");
return { name: cols[0], value: parseFloat(cols[1]) };
});
return Response.json(rows);
}A alternativa correta: faça upload para R2 (object storage da Cloudflare), processe com um Worker de plano paid (30s de CPU) ou delegue para um serviço externo. O Worker na borda recebe o upload e enfileira.
Erro 3: hardcodar o namespace ID do KV no código.
# ERRADO: mesmo namespace para dev e prod
[[kv_namespaces]]
binding = "CACHE_KV"
id = "abc123"# CORRETO: namespace separado para dev local
[[kv_namespaces]]
binding = "CACHE_KV"
id = "abc123-production"
# Preview namespace usado pelo `wrangler dev`
[env.dev.kv_namespaces]
binding = "CACHE_KV"
id = "def456-development"Deploy e CI/CD com GitHub Actions
Deploy manual:
npx wrangler deploy
# Publica em <nome>.workers.dev em ~10 segundosPara CI/CD, o Wrangler aceita token via variável de ambiente. Crie um API token no painel da Cloudflare com permissão "Workers Scripts: Edit":
# .github/workflows/deploy.yml
name: Deploy Worker
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
# CLOUDFLARE_API_TOKEN vem dos secrets do repositório
- run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}O deploy inteiro leva menos de 30 segundos no CI. Sem build de container, sem push para registry, sem rollout de pods. Se algo quebrar, npx wrangler rollback reverte para a versão anterior.
Para monitorar logs em tempo real após o deploy:
# Tail de logs: mostra requests, console.log e erros em tempo real
npx wrangler tail --format prettyIsso substitui, para Workers simples, a necessidade de configurar um stack de observabilidade separado. Para APIs maiores, integre com Logpush da Cloudflare ou envie logs para um serviço externo via fetch dentro do Worker.
Quando Workers faz sentido e quando não faz
Workers é a escolha certa para: proxies de API que adicionam auth/headers/cache, endpoints de webhook que validam e encaminham, APIs de leitura com KV como cache, redirect engines, feature flags na borda, A/B testing por geolocalização (request.cf.country).
Workers é a escolha errada para: APIs que precisam de PostgreSQL com queries complexas (a latência para o banco mata o ganho da edge), processamento CPU-heavy acima de 30 segundos, aplicações que dependem de bibliotecas Node.js com binding nativo (sharp, bcrypt com C++, puppeteer).
Se a sua API precisa de banco relacional e a latência para o banco é o gargalo, considere D1 (SQLite distribuído da Cloudflare) para casos simples, ou Hyperdrive (connection pooler da Cloudflare para PostgreSQL externo) para casos mais pesados, similar ao que descrevemos em escalar PostgreSQL com connection pooling.
Para quem já tem um backend Node.js rodando e quer entender onde Workers se encaixa na arquitetura, o post sobre APIs reais que não quebram em produção cobre o lado do servidor de origem. Workers funciona como camada na frente: cache, auth, rate limit, roteamento.
O fluxo de CLI com Wrangler segue a mesma filosofia de ferramentas de linha de comando que automatizam trabalho repetitivo: um comando faz uma coisa, faz bem, e integra com pipe. wrangler dev para local, wrangler deploy para produção, wrangler tail para debug. Se você constrói CLIs próprias que interagem com a Cloudflare API, o post sobre CLI com IA mostra patterns de estruturação que se aplicam aqui.
Para o código de fetch resiliente entre seu Worker e APIs externas, Fetch, Retry e Timeout cobre os patterns de retry e circuit breaker que funcionam dentro de Workers (com a ressalva de que setTimeout existe mas setInterval não persiste entre requests). E se você quer entender por que closures e protótipos se comportam como se comportam no V8 isolate, as mecânicas do JavaScript que você usa sem entender dá o contexto.
FAQ
Workers roda Node.js?
Não. Workers roda V8 isolates diretamente, sem a camada do Node.js (sem libuv, sem event loop do Node, sem process, sem Buffer nativo). A flag nodejs_compat adiciona polyfills para algumas APIs (Buffer, crypto, util), mas não é Node.js completo. Se seu código depende de fs, net, child_process ou qualquer módulo que toca o sistema operacional, não vai funcionar.
Qual a diferença entre KV, R2, D1 e Durable Objects?
KV é key-value eventually consistent, otimizado para leitura (cache, flags, configuração). R2 é object storage compatível com S3 (arquivos, uploads, assets). D1 é SQLite distribuído para queries relacionais simples. Durable Objects dão estado consistente por instância (contadores exatos, WebSocket rooms, locks). A escolha depende do padrão de acesso: leitura pesada usa KV, escrita consistente usa Durable Objects, queries SQL usam D1.
Quanto custa em produção?
O plano free dá 100.000 requests por dia, 10ms de CPU time por request, KV com 100.000 leituras/dia. O plano paid (Workers Paid, US$5/mês) dá 10 milhões de requests/mês inclusos, 30 segundos de CPU time, e cobra US$0.50 por milhão de requests adicionais. Para a maioria das APIs de borda, o custo fica abaixo de US$10/mês até volumes significativos.
Posso usar TypeScript?
Sim. Wrangler compila TypeScript nativamente. Troque src/index.js por src/index.ts, ajuste o main no wrangler.toml, e o Wrangler cuida do build. Os tipos da Cloudflare estão em @cloudflare/workers-types.
Como faço rollback se o deploy quebrar?
npx wrangler rollback reverte para a versão anterior. npx wrangler deployments list mostra o histórico. Cada deploy é atômico e global: ou todas as 300+ regiões recebem a nova versão, ou nenhuma recebe.
A posição que defendo
Cloudflare Workers, operado via Wrangler CLI, é a melhor relação custo/complexidade para APIs de borda em 2025. Não para tudo: para backends com banco relacional, filas e jobs longos, continue com containers. Mas para a camada que fica entre o cliente e seu backend, a que valida tokens, cacheia respostas, roteia por geolocalização e responde health checks, Workers elimina infraestrutura que você não deveria estar gerenciando. O deploy de 10 segundos via CLI, sem Dockerfile, sem Helm chart, sem cold start perceptível, muda o que significa "colocar em produção" para essa classe de serviço.

Escrito por
Marcos Soares
Fullstack Developer · CEO da Agência Poti
Fullstack Developer e CEO da Agência Poti. Mais de 20 anos construindo arquiteturas cloud-native com React, Next.js e sistemas distribuídos. Parceiro comercial do estúdio iellou design. Fundador do Vivo de Código.
Comentários
Participe da discussão
Seja o primeiro a comentar!
Continue Aprofundando
Guias de integração relacionados
Conteúdo técnico toda semana
Receba artigos sobre arquitetura, padrões de projeto e engenharia de software. Direto no seu e-mail, sem enrolação.
Sem spam. Cancele a qualquer momento com 1 clique.


