Ir para o conteúdo
Infra

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

Marcos Soares
Atualizado em 
12 minutos de leitura
Ilustracao 3D de prisma de vidro fosco com filamentos luminosos laranjas irradiando para nodes na edge representando Cloud...
Ouça este artigo
0:00Cloudflare 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.

CapacidadeWrangler CLIDocker + K8sServerless (AWS Lambda)
Cold start típico< 5msN/A (container quente)100-500ms (Node.js)
Deploy time5-15 segundosminutos30-60 segundos
Regiões300+ PoPs automáticovocê escolhevocê escolhe
Storage integradoKV, R2, D1, Durable ObjectsexternoDynamoDB, S3
Custo em idlezero (free tier: 100k req/dia)container rodandozero
RuntimeV8 isolates (não é Node.js)qualquerNode.js, Python, etc.
Limite de CPU time10ms (free) / 30s (paid)sem limite15 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):

Bash
npm init -y
npm install --save-dev wrangler
npx wrangler --version

Crie o projeto com scaffold mínimo:

Bash
npx wrangler init api-edge --type javascript
cd api-edge

O arquivo wrangler.toml gerado é o centro de configuração. Ajuste para algo funcional:

TOML
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:

JAVASCRIPT
// 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:

Bash
npx wrangler dev
# Escuta em http://localhost:8787
# Miniflare simula KV, secrets e bindings localmente

Teste com curl:

Bash
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:

Bash
# 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 list

No código, secrets aparecem no objeto env da mesma forma que variáveis:

JAVASCRIPT
// 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.

JAVASCRIPT
// 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;
}
JAVASCRIPT
// 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.

JAVASCRIPT
// 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.

TOML
# ERRADO: mesmo namespace para dev e prod
[[kv_namespaces]]
binding = "CACHE_KV"
id = "abc123"
TOML
# 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:

Bash
npx wrangler deploy
# Publica em <nome>.workers.dev em ~10 segundos

Para 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":

YAML
# .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:

Bash
# Tail de logs: mostra requests, console.log e erros em tempo real
npx wrangler tail --format pretty

Isso 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.

Marcos Soares

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.