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:

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);
  },
};

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:

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,
    });
  },
};

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:

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 });
  },
};
Debug-tip: Als een client claimt "jullie API is stuk" maar de Worker een 400 laat zien, is in negen van de tien gevallen hun JSON misvormd — trailing komma, key zonder quotes, of per ongeluk een BOM-karakter aan het begin. Vraag ze om hun payload in de JSON Validator te plakken en het probleem komt meestal in minder dan een minuut boven water.

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:

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 });
  },
};

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:

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);
  },
};

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:

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;
  },
};

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:

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"}'

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.