A primeira vez que fiz deploy de uma API JSON no Cloudflare Workers, publiquei do meu laptop às 23h e ela já estava rodando em mais de 300 data centers antes de eu terminar o chá. Sem Dockerfile, sem cluster Kubernetes, sem drama de cold-start. Um único wrangler deploy e um bundle de 1,2 KB. Essa experiência é a razão pela qual Workers virou meu padrão para serviços JSON-entra, JSON-sai — webhooks, proxies, agregadores de API, autenticação de borda. Se 80% do seu backend é "parsear JSON, fazer alguma coisa, retornar JSON", esse é o artigo que eu queria ter tido quando comecei.

Um Cloudflare Worker é basicamente uma função JavaScript (ou TypeScript) única que roda em isolates V8 na borda da Cloudflare. Ele recebe uma Request, retorna uma Response, e tem acesso à API Fetch padrão. Se você já usou fetch() no navegador, já conhece 90% do runtime. O que você não conhece é o pequeno conjunto de padrões que separa um Worker de brinquedo de um que você consegue rodar de verdade em produção. É disso que trata este artigo.

Seu primeiro endpoint JSON

Aqui está o menor endpoint JSON útil. Ele retorna um único objeto com timestamp e mensagem. Salve como src/index.ts num projeto Wrangler:

js
export default {
  async fetch(request, env, ctx) {
    const payload = {
      message: 'Hello from the edge',
      servedAt: new Date().toISOString(),
      colo: request.cf?.colo ?? 'unknown',
    };

    return Response.json(payload);
  },
};

Duas coisas para notar. Primeiro, Response.json() é um helper estático que serializa o objeto e define Content-Type: application/json para você. Não invente seu próprio new Response(JSON.stringify(x)) a menos que precise de um content type custom — você vai acabar esquecendo o header em algum momento. Segundo, request.cf.colo te diz qual data center da Cloudflare está servindo a requisição. Uma requisição de Berlim mostra FRA, de Tóquio mostra NRT. É a pegada de "edge" inteira num único campo.

Parseando um body de requisição JSON

Endpoints POST precisam ler um body. A Fetch API te dá request.json(), que lê a stream do body e faz o parse numa única chamada:

js
export default {
  async fetch(request) {
    if (request.method !== 'POST') {
      return new Response('Method not allowed', { status: 405 });
    }

    const body = await request.json();

    // body is now a regular JavaScript object
    const { email, plan } = body;

    return Response.json({
      received: { email, plan },
      ok: true,
    });
  },
};

Parece limpo, mas esse código tem um bug que você vai pegar dentro de 24h depois de colocar no ar: se o cliente mandar um body vazio ou JSON malformado, request.json() lança um SyntaxError, seu Worker quebra, e a Cloudflare retorna um 500 genérico. Não é a resposta que você quer mostrar para os clientes.

Lidando com JSON malformado — não deixe dar 500

Sempre embrulhe o parse do body num try/catch e retorne um 400 decente. Aqui está o padrão que eu uso em todo Worker:

js
async function readJson(request) {
  try {
    return { ok: true, data: await request.json() };
  } catch (err) {
    return {
      ok: false,
      error: 'Invalid JSON body',
      detail: err.message,
    };
  }
}

export default {
  async fetch(request) {
    const result = await readJson(request);

    if (!result.ok) {
      return Response.json(result, { status: 400 });
    }

    const { email, plan } = result.data;

    if (!email || !plan) {
      return Response.json(
        { ok: false, error: 'email and plan are required' },
        { status: 422 },
      );
    }

    return Response.json({ ok: true, email, plan });
  },
};
Dica de debug: quando um cliente diz "sua API está quebrada" mas o Worker mostra um 400, nove em cada dez vezes o JSON deles está malformado — vírgula sobrando, chave sem aspas, ou um BOM acidental no início. Peça para eles colarem o payload no JSON Validator e o problema normalmente aparece em menos de um minuto.

CORS para APIs JSON

Se seu Worker for chamado por um navegador de uma origem diferente — o caso normal — você precisa de headers CORS. Navegadores mandam um preflight OPTIONS antes da requisição real para qualquer coisa mais complexa que um GET simples. Trate os dois num lugar só:

js
const CORS_HEADERS = {
  'Access-Control-Allow-Origin': 'https://app.example.com',
  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  'Access-Control-Max-Age': '86400',
};

export default {
  async fetch(request) {
    if (request.method === 'OPTIONS') {
      return new Response(null, { status: 204, headers: CORS_HEADERS });
    }

    const data = { ping: 'pong', at: Date.now() };

    return Response.json(data, { headers: CORS_HEADERS });
  },
};

Evite Access-Control-Allow-Origin: * em qualquer coisa que leia credenciais ou retorne dados de usuário. É um daqueles atalhos que parece inofensivo em dev e vira um incidente de segurança em produção. Fixe as origens que você realmente serve no código, ou leia de uma allowlist em env.

Encaminhando JSON para uma API upstream

Um dos usos mais comuns de um Worker é como proxy fino: esconder uma API key, remodelar uma resposta, remover campos que o cliente não precisa, ou costurar duas chamadas upstream numa só. Aqui está um Worker que chama um serviço upstream, pega só os campos que interessam, e retorna um payload JSON mais limpo:

js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const userId = url.searchParams.get('id');

    if (!userId) {
      return Response.json({ error: 'id required' }, { status: 400 });
    }

    const upstream = await fetch(
      `https://api.internal.example.com/users/${userId}`,
      {
        headers: { 'Authorization': `Bearer ${env.UPSTREAM_TOKEN}` },
      },
    );

    if (!upstream.ok) {
      return Response.json(
        { error: 'upstream failed', status: upstream.status },
        { status: 502 },
      );
    }

    const full = await upstream.json();

    // Strip internal fields before returning to the client
    const safe = {
      id: full.id,
      displayName: full.display_name,
      avatarUrl: full.avatar_url,
      joinedAt: full.created_at,
    };

    return Response.json(safe);
  },
};

Duas coisas para prestar atenção. Primeiro, sempre cheque upstream.ok antes de chamar .json() — um 500 do upstream vai ter HTML ou uma página de erro, e chamar .json() nele dá o mesmo erro que qualquer outro JSON malformado. Segundo, guarde segredos como UPSTREAM_TOKEN nos secrets do Wrangler (wrangler secret put UPSTREAM_TOKEN) — nunca no wrangler.toml e nunca commitado no git.

Cacheando respostas JSON na borda

Quando um upstream é lento ou caro, a Cache API te deixa memoizar JSON na borda. Cada data center mantém seu próprio cache, então o primeiro usuário em Frankfurt paga o custo do upstream, e os próximos 10.000 pegam em menos de 5ms da RAM ali perto:

js
export default {
  async fetch(request, env, ctx) {
    const cache = caches.default;
    const cacheKey = new Request(request.url, request);

    let response = await cache.match(cacheKey);
    if (response) {
      return response;
    }

    const upstream = await fetch('https://api.example.com/popular-items');
    const data = await upstream.json();

    response = Response.json(data, {
      headers: {
        'Cache-Control': 'public, max-age=60',
      },
    });

    // Don't block the response on the cache write
    ctx.waitUntil(cache.put(cacheKey, response.clone()));

    return response;
  },
};

O ctx.waitUntil() é a parte menos óbvia. Sem ele, o cache.put() é aguardado e sua resposta espera uma escrita em disco com a qual ela não precisa se importar. waitUntil deixa você retornar a resposta imediatamente enquanto o runtime mantém a escrita do cache viva em background. É o mesmo padrão que você usaria para beacons de analytics, forward de logs, qualquer coisa fire-and-forget.

Desenvolvimento local com Wrangler

Você não precisa de uma conta Cloudflare para iterar. Instale o Wrangler, crie um projeto, e você tem um runtime local de Workers que bate de perto com produção:

bash
npm create cloudflare@latest my-json-api
cd my-json-api
npm run dev

# Worker is now live at http://localhost:8787
# Hit it from another terminal:
curl -X POST http://localhost:8787 \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","plan":"pro"}'

O runtime local usa o workerd, o mesmo motor que a Cloudflare roda em produção. Os comportamentos que diferem (latência de KV, semântica de cache, campos de request.cf) são bem documentados e raramente te mordem em APIs JSON simples. Faça deploy com wrangler deploy e o mesmo código fica no ar globalmente em segundos.

Ferramentas úteis para construir APIs JSON no Worker

Algumas ferramentas às quais recorro constantemente ao construir Workers que lidam com JSON: JSON Formatter para deixar bonita uma resposta upstream feia que eu estou tentando fazer engenharia reversa, JSON Validator quando um payload POST falha e eu preciso saber exatamente onde, JSON Path para planejar a lógica de seleção de campos antes de escrever, e JSON Minifier quando quero checar se o tamanho do payload importa de verdade para um endpoint específico.

O formato JSON em si está especificado no RFC 8259 — vale uma lida rápida se você encontrar um caso de borda do tipo "meu parser aceita NaN?" (resposta: não deveria). A própria galeria de exemplos de Workers da Cloudflare tem receitas para verificação de JWT, testes A/B, reescrita de HTML, e mais uma dúzia de padrões depois que você superar o básico deste artigo.

Encerrando

Cloudflare Workers é uma combinação muito boa para APIs JSON — pequenas, rápidas, distribuídas globalmente, e baratas o suficiente para deixar projetos paralelos rodando. O caminho feliz é só request.json() e Response.json(), mas o caminho de produção envolve quatro hábitos extras: embrulhar o parse do body em try/catch, adicionar headers CORS intencionalmente, checar upstream.ok antes de parsear respostas proxeadas, e usar ctx.waitUntil para escritas no cache e outros trabalhos em background. Acerte esses quatro e você vai entregar Workers que ficam de pé.