De eerste keer dat ik een JSON API uitrolde op
Cloudflare Workers,
deployde ik hem om 23:00 vanaf mijn laptop en draaide hij in 300+ datacenters voordat ik mijn thee op had.
Geen Dockerfile, geen Kubernetes-cluster, geen cold-start-drama. Eén wrangler deploy
en een bundle van 1,2 KB. Die ervaring is de reden dat Workers mijn default is geworden voor JSON-in, JSON-out services —
webhooks, proxies, API-aggregators, edge auth. Als 80% van je backend neerkomt op "parse JSON, doe iets, stuur JSON terug",
is dit het artikel dat ik had willen hebben toen ik begon.
Een Cloudflare Worker is in wezen één JavaScript- (of TypeScript-)functie die draait op V8-isolates aan de edge van Cloudflare. Hij ontvangt een Request, geeft een Response terug, en heeft toegang tot de standaard Fetch API. Als je fetch() in een browser hebt gebruikt, ken je 90% van de runtime al. Wat je niet kent, is die kleine set patronen die een speelgoed-Worker scheiden van een die je echt in productie kunt draaien. Daar gaat dit artikel over.
Je eerste JSON-endpoint
Hier is het kleinst nuttige JSON-endpoint. Het geeft één object terug met een timestamp en een
bericht. Sla het op als src/index.ts in een Wrangler-project:
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);
},
};Twee dingen om op te merken. Ten eerste, Response.json() is een statische helper die
het object serialiseert en Content-Type: application/json voor je zet. Maak niet je eigen
new Response(JSON.stringify(x)) tenzij je een custom content type nodig hebt — je vergeet
die header uiteindelijk toch. Ten tweede, request.cf.colo vertelt je welk Cloudflare-
datacenter het request afhandelt. Een request uit Berlijn laat FRA zien,
uit Tokyo NRT. Dat is de hele "edge"-pitch in één veld.
Een JSON-request body parsen
POST-endpoints moeten een body lezen. De Fetch API geeft je request.json(), die de body-stream leest en hem in één call parset:
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,
});
},
};Ziet er netjes uit, maar deze code heeft een bug die je binnen 24 uur na het uitrollen raakt:
als de client een lege body of misvormde JSON stuurt, gooit request.json()
een SyntaxError, crasht je Worker, en geeft Cloudflare een generieke 500 terug.
Dat is niet het antwoord dat je voor klanten wilt tonen.
Omgaan met misvormde JSON — laat het geen 500 worden
Wikkel het parsen van de body altijd in een try/catch en geef een nette 400 terug. Dit is het patroon dat ik in elke Worker gebruik:
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 voor JSON API's
Als je Worker wordt aangeroepen vanuit een browser op een andere origin — wat
het normale geval is — heb je
CORS-
headers nodig. Browsers sturen een OPTIONS-preflight voor het echte request, voor alles
wat meer is dan een simpele GET. Handel beide op één plek af:
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 });
},
};Vermijd Access-Control-Allow-Origin: * op alles dat credentials leest
of gebruikersdata teruggeeft. Dat is een van die shortcuts die in dev onschuldig lijkt en in productie verandert
in een security-incident. Hardcode de origins die je daadwerkelijk bedient, of lees ze uit een
allow-list in env.
JSON doorsturen naar een upstream API
Een van de meest voorkomende toepassingen van een Worker is als dunne proxy: verberg een API-key, herschik een response, strip velden die een client niet nodig heeft, of combineer twee upstream-calls tot één. Hier is een Worker die een upstream-service aanroept, alleen de velden kiest die we interesseren, en een schonere JSON-payload teruggeeft:
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);
},
};Twee dingen om op te letten. Ten eerste, check altijd upstream.ok
voordat je .json() aanroept — een 500 van upstream bevat HTML of een errorpagina,
en .json() daarop aanroepen gooit dezelfde fout als elke andere misvormde JSON.
Ten tweede, bewaar secrets zoals UPSTREAM_TOKEN in Wrangler-secrets
(wrangler secret put UPSTREAM_TOKEN) — nooit in wrangler.toml
en nooit in git commit.
JSON-responses cachen aan de edge
Wanneer een upstream traag of duur is, laat de Cache API je JSON aan de edge memoizen. Elk datacenter houdt zijn eigen cache, dus de eerste gebruiker in Frankfurt betaalt de upstream-kosten, en de volgende 10.000 krijgen het in minder dan 5ms uit nabije RAM:
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;
},
};Het ctx.waitUntil() is het minder voor de hand liggende stuk. Zonder dat wordt de
cache.put() ge-awaited en wacht je response op een schrijfactie naar disk die hem niet
aangaat. waitUntil laat je de response onmiddellijk teruggeven
terwijl de runtime de cache-schrijfactie op de achtergrond in leven houdt. Het is hetzelfde patroon
dat je zou gebruiken voor analytics-beacons, log-forwarding, alles wat fire-and-forget is.
Lokale ontwikkeling met Wrangler
Je hebt geen Cloudflare-account nodig om te itereren. Installeer Wrangler, scaffold een project en je krijgt een lokale Workers-runtime die sterk overeenkomt met productie:
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"}'De lokale runtime gebruikt workerd, dezelfde engine die Cloudflare in productie draait.
De gedragingen die verschillen (KV-latency, cache-semantiek, request.cf-velden)
zijn goed gedocumenteerd en bijten je zelden bij eenvoudige JSON-API's. Deploy met
wrangler deploy en dezelfde code is in seconden wereldwijd live.
Handige tools voor het bouwen van Worker-JSON-API's
Een paar tools waar ik constant naar grijp bij het bouwen van Workers die met JSON werken: JSON Formatter om een lelijke upstream-response pretty te printen die ik aan het reverse-engineeren ben, JSON Validator als een POST-payload faalt en ik precies moet weten waar, JSON Path om de veld-picking-logica te plannen voor ik hem schrijf, en JSON Minifier als ik wil checken of de wire-size er echt toe doet voor een gegeven endpoint.
Het JSON-formaat zelf is gespecificeerd in
RFC 8259
— de moeite van een snelle scan waard als je ooit tegen een edge case aanloopt als "laat mijn parser NaN toe?"
(antwoord: dat zou hij niet moeten). Cloudflare's eigen
Workers-voorbeeldengallerij
heeft recepten voor JWT-verificatie, A/B-testing, HTML-rewriting en een dozijn andere patronen
zodra je de basics in dit artikel bent ontgroeid.
Afronding
Cloudflare Workers past heel goed bij JSON API's — klein, snel, wereldwijd
gedistribueerd en goedkoop genoeg om side-projects te laten draaien. Het happy path is
gewoon request.json() en Response.json(), maar het productiepad
omvat vier extra gewoontes: wikkel het parsen van de body in try/catch, zet CORS-headers bewust,
check upstream.ok voordat je geproxyde responses parset, en gebruik
ctx.waitUntil voor cache-schrijfacties en ander achtergrondwerk. Doe die vier goed
en je rolt Workers uit die blijven draaien.