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:
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:
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:
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 });
},
};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ó:
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:
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:
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:
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é.