La prima volta che ho messo in produzione una JSON API su Cloudflare Workers, l'ho deployata dal mio portatile alle 11 di sera e girava su oltre 300 data center prima che finissi il tè. Niente Dockerfile, niente cluster Kubernetes, niente dramma da cold-start. Un singolo wrangler deploy e un bundle da 1,2 KB. Quell'esperienza è il motivo per cui Workers è diventato il mio default per i servizi JSON-in, JSON-out — webhook, proxy, aggregatori di API, auth perimetrale. Se l'80% del tuo backend è "parsifica JSON, fa' qualcosa, restituisci JSON", questo è l'articolo che avrei voluto avere quando ho iniziato.

Un Cloudflare Worker è sostanzialmente una singola funzione JavaScript (o TypeScript) che gira su isolati V8 sulla rete edge di Cloudflare. Riceve una Request, restituisce una Response, e ha accesso alla Fetch API standard. Se hai usato fetch() in un browser, conosci già il 90% del runtime. Quello che non conosci è il piccolo insieme di pattern che separano un Worker giocattolo da uno che puoi davvero mandare in produzione. Di questo parla l'articolo.

Il tuo primo endpoint JSON

Ecco l'endpoint JSON utile più piccolo. Restituisce un singolo oggetto con un timestamp e un messaggio. Salvalo come src/index.ts in un progetto 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);
  },
};

Due cose da notare. Primo, Response.json() è un helper statico che serializza l'oggetto e imposta Content-Type: application/json per te. Non farti un tuo new Response(JSON.stringify(x)) a meno che non ti serva un content type personalizzato — prima o poi ti dimenticherai l'header. Secondo, request.cf.colo ti dice quale data center Cloudflare sta servendo la richiesta. Una richiesta da Berlino mostrerà FRA, da Tokyo mostrerà NRT. È l'intera proposta "edge" in un singolo campo.

Parsificare il body di una richiesta JSON

Gli endpoint POST devono leggere un body. La Fetch API ti offre request.json(), che legge lo stream del body e lo parsifica in una sola chiamata:

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

Sembra pulito, ma questo codice ha un bug che incontrerai entro 24 ore dal deploy: se il client invia un body vuoto o JSON malformato, request.json() lancia un SyntaxError, il tuo Worker crasha e Cloudflare restituisce un generico 500. Non è la risposta che vuoi mostrare ai clienti.

Gestire JSON malformato — non lasciare che diventi un 500

Avvolgi sempre il parsing del body in un try/catch e restituisci un 400 come si deve. Ecco il pattern che uso in ogni 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 });
  },
};
Dritta per il debug: quando un client sostiene che "la vostra API è rotta" ma il Worker mostra un 400, nove volte su dieci il loro JSON è malformato — virgola finale, chiave senza virgolette, o un carattere BOM accidentale all'inizio. Chiedi loro di incollare il payload nel JSON Validator e di solito il problema salta fuori in meno di un minuto.

CORS per le JSON API

Se il tuo Worker sarà chiamato da un browser su un'origine diversa — che è il caso normale — ti servono gli header CORS. I browser inviano un preflight OPTIONS prima della richiesta vera per tutto ciò che è più di una semplice GET. Gestisci entrambi in un unico posto:

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

Evita Access-Control-Allow-Origin: * su qualsiasi cosa che legga credenziali o restituisca dati utente. È una di quelle scorciatoie che in dev sembra innocua e in prod si trasforma in un incidente di sicurezza. Codifica le origin che effettivamente servi, o leggile da una allow-list in env.

Inoltrare JSON a una API upstream

Uno degli usi più comuni di un Worker è come proxy leggero: nascondere una API key, rimodellare una risposta, togliere campi che al client non servono, o cucire insieme due chiamate upstream. Ecco un Worker che chiama un servizio upstream, seleziona solo i campi che ci interessano e restituisce un payload JSON più pulito:

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

Due cose a cui prestare attenzione. Primo, controlla sempre upstream.ok prima di chiamare .json() — un 500 dall'upstream avrà HTML o una pagina di errore, e chiamarci sopra .json() solleva lo stesso tipo di eccezione di qualsiasi altro JSON malformato. Secondo, tieni i segreti come UPSTREAM_TOKEN nei Wrangler secrets (wrangler secret put UPSTREAM_TOKEN) — mai in wrangler.toml e mai committati su git.

Mettere in cache le risposte JSON sull'edge

Quando un upstream è lento o costoso, la Cache API ti permette di memoizzare il JSON sull'edge. Ogni data center tiene la sua cache, quindi il primo utente a Francoforte paga il costo upstream e i successivi 10.000 lo ottengono in meno di 5ms dalla RAM vicina:

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

Il ctx.waitUntil() è la parte meno ovvia. Senza, il cache.put() viene atteso e la tua risposta aspetta una scrittura su disco di cui non le importa nulla. waitUntil ti permette di restituire la risposta immediatamente mentre il runtime tiene viva la scrittura in cache in background. È lo stesso pattern che useresti per i beacon di analytics, l'inoltro di log, qualsiasi cosa fire-and-forget.

Sviluppo locale con Wrangler

Non ti serve un account Cloudflare per iterare. Installa Wrangler, fai lo scaffold di un progetto e ottieni un runtime Workers locale che corrisponde fedelmente alla produzione:

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

Il runtime locale usa workerd, lo stesso motore che Cloudflare usa in produzione. I comportamenti che differiscono (latenza di KV, semantica della cache, campi di request.cf) sono ben documentati e raramente ti mordono per semplici JSON API. Fai il deploy con wrangler deploy e lo stesso codice è live globalmente in pochi secondi.

Strumenti utili per costruire JSON API con i Worker

Alcuni strumenti a cui ricorro di continuo quando costruisco Worker che lavorano con JSON: JSON Formatter per formattare una brutta risposta upstream che sto cercando di capire, JSON Validator quando un payload POST fallisce e mi serve sapere esattamente dove, JSON Path per pianificare la logica di selezione dei campi prima di scriverla, e JSON Minifier quando voglio controllare se la dimensione sulla rete conta davvero per un dato endpoint.

Il formato JSON di per sé è specificato nell' RFC 8259 — vale una lettura veloce se mai incontri un edge case tipo "il mio parser accetta NaN?" (risposta: non dovrebbe). La galleria di esempi Workers di Cloudflare ha ricette per verifica JWT, A/B testing, riscrittura HTML e una dozzina di altri pattern una volta superate le basi di questo articolo.

Tiriamo le somme

Cloudflare Workers è una soluzione davvero adatta alle JSON API — piccole, veloci, distribuite globalmente e abbastanza economiche da poter lasciare i side project in esecuzione. Il percorso felice è solo request.json() e Response.json(), ma il percorso produzione coinvolge quattro abitudini extra: avvolgere il parsing del body in try/catch, aggiungere gli header CORS intenzionalmente, controllare upstream.ok prima di parsificare risposte proxate, e usare ctx.waitUntil per le scritture in cache e altro lavoro in background. Indovina quelle quattro e spedirai Worker che restano su.