Kiedy pierwszy raz wypuściłem JSON API na Cloudflare Workers, zdeployowałem je z laptopa o 23:00 i działało w ponad 300 data center, zanim skończyłem herbatę. Żadnego Dockerfile'a, żadnego klastra Kubernetes, żadnego dramatu z cold startami. Pojedyncze wrangler deploy i bundle ważący 1,2 KB. To doświadczenie sprawiło, że Workers stały się moim domyślnym wyborem dla serwisów typu JSON-in, JSON-out — webhooków, proxy, agregatorów API, auth na krawędzi. Jeśli 80% twojego backendu to "sparsuj JSON, zrób coś, zwróć JSON", to jest artykuł, którego potrzebowałem na początku.

Cloudflare Worker to zasadniczo pojedyncza funkcja JavaScript (albo TypeScript) działająca na izolatach V8 na krawędzi sieci Cloudflare. Przyjmuje Request, zwraca Response i ma dostęp do standardowego Fetch API. Jeśli używałeś fetch() w przeglądarce, znasz już 90% runtime'u. Nie znasz natomiast małego zestawu wzorców, które oddzielają zabawkowego Workera od takiego, którego faktycznie możesz puścić na produkcję. O tym jest ten artykuł.

Twój pierwszy endpoint JSON

Oto najmniejszy użyteczny endpoint JSON. Zwraca pojedynczy obiekt z timestampem i wiadomością. Zapisz to jako src/index.ts w projekcie Wranglera:

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

Dwie rzeczy warte uwagi. Po pierwsze, Response.json() to statyczny helper, który serializuje obiekt i ustawia za ciebie Content-Type: application/json. Nie rób własnego new Response(JSON.stringify(x)), chyba że potrzebujesz niestandardowego content type — prędzej czy później zapomnisz o nagłówku. Po drugie, request.cf.colo mówi ci, które data center Cloudflare obsługuje żądanie. Request z Berlina pokaże FRA, z Tokio NRT. Cały "edge pitch" w jednym polu.

Parsowanie ciała żądania JSON

Endpointy POST muszą czytać body. Fetch API daje ci request.json(), które czyta strumień body i parsuje go w jednym wywołaniu:

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

Wygląda czysto, ale ten kod ma buga, na którego wpadniesz w ciągu 24 godzin od wypuszczenia: jeśli klient wyśle puste body albo niepoprawny JSON, request.json() rzuci SyntaxError, twój Worker się wywali, a Cloudflare zwróci ogólny 500. To nie jest odpowiedź, którą chcesz pokazywać klientom.

Obsługa niepoprawnego JSON-a — nie daj mu zwrócić 500

Zawsze zawijaj parsowanie body w try/catch i zwracaj porządne 400. Oto wzorzec, którego używam w każdym Workerze:

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 });
  },
};
Debugging tip: kiedy klient twierdzi "wasze API jest zepsute", ale Worker pokazuje 400, w dziewięciu przypadkach na dziesięć ich JSON jest niepoprawny — końcowy przecinek, klucz bez cudzysłowów albo przypadkowy BOM na początku. Poproś ich, żeby wkleili swój payload do Walidatora JSON, a problem zwykle wyłazi w minutę.

CORS dla API JSON

Jeśli twój Worker ma być wołany z przeglądarki z innego origin — co jest normalnym scenariuszem — potrzebujesz nagłówków CORS. Przeglądarki wysyłają preflight OPTIONS przed właściwym żądaniem dla wszystkiego poza prostym GET-em. Obsłuż jedno i drugie w jednym miejscu:

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

Unikaj Access-Control-Allow-Origin: * na czymkolwiek, co czyta credentiale albo zwraca dane użytkownika. To jeden z tych skrótów, który wygląda niewinnie w dev i zamienia się w incydent bezpieczeństwa na produkcji. Hardkoduj origin-y, które faktycznie obsługujesz, albo czytaj je z allow-listy w env.

Przekazywanie JSON-a do API upstream

Jedno z najczęstszych zastosowań Workera to cienkie proxy: ukryj klucz API, przekształć odpowiedź, odetnij pola, których klient nie potrzebuje, albo połącz dwa wywołania upstream w jedno. Oto Worker, który woła serwis upstream, wybiera tylko pola, które nas obchodzą, i zwraca czystszy payload JSON:

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

Dwie rzeczy, na które warto uważać. Po pierwsze, zawsze sprawdzaj upstream.ok przed wywołaniem .json() — 500 z upstream będzie miał HTML albo stronę błędu, a wywołanie .json() na tym rzuci tak samo jak przy każdym innym niepoprawnym JSON-ie. Po drugie, trzymaj sekrety jak UPSTREAM_TOKEN w Wrangler secrets (wrangler secret put UPSTREAM_TOKEN) — nigdy w wrangler.toml i nigdy nie commituj ich do gita.

Cachowanie odpowiedzi JSON na krawędzi

Kiedy upstream jest wolny albo drogi, Cache API pozwala memoizować JSON na krawędzi. Każde data center ma własny cache, więc pierwszy użytkownik z Frankfurtu płaci koszt upstream, a kolejnych 10 000 dostaje odpowiedź w mniej niż 5 ms z RAM-u obok:

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

ctx.waitUntil() to nieoczywista część. Bez niego cache.put() jest awaitowany i twoja odpowiedź czeka na zapis do dysku, o który w ogóle nie musi się martwić. waitUntil pozwala zwrócić odpowiedź natychmiast, podczas gdy runtime podtrzymuje zapis do cache'a w tle. To ten sam wzorzec, którego użyjesz do beaconów analitycznych, forwardowania logów — czegokolwiek typu fire-and-forget.

Lokalny development z Wranglerem

Nie potrzebujesz konta Cloudflare, żeby iterować. Zainstaluj Wranglera, zescaffolduj projekt i dostajesz lokalny runtime Workers, który jest bardzo bliski produkcji:

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

Lokalny runtime używa workerd, tego samego silnika, który Cloudflare puszcza na produkcji. Zachowania, które się różnią (latencja KV, semantyka cache, pola request.cf) są dobrze udokumentowane i rzadko cię kąsają dla prostych API JSON. Zdeployuj przez wrangler deploy i ten sam kod jest globalnie live w kilka sekund.

Przydatne narzędzia do budowania API JSON w Workerach

Kilka narzędzi, po które sięgam nieustannie, budując Workery operujące na JSON-ie: JSON Formatter do pretty-printowania brzydkiej odpowiedzi upstream, którą próbuję rozgryźć, JSON Validator, kiedy payload POST wysypuje się i muszę wiedzieć dokładnie gdzie, JSON Path do zaplanowania logiki wybierania pól, zanim ją napiszę, oraz JSON Minifier, kiedy chcę sprawdzić, czy rozmiar na drucie faktycznie ma znaczenie dla danego endpointu.

Sam format JSON jest wyspecyfikowany w RFC 8259 — warto go przejrzeć, jeśli kiedyś wpadniesz na edge case typu "czy mój parser pozwala na NaN?" (odpowiedź: nie powinien). Własna galeria przykładów Workers Cloudflare'a ma przepisy na weryfikację JWT, testy A/B, przepisywanie HTML i tuzin innych wzorców na moment, kiedy przerośniesz podstawy z tego artykułu.

Podsumowanie

Cloudflare Workers to bardzo dobry wybór dla API JSON — małe, szybkie, globalnie rozproszone i na tyle tanie, że można zostawić side-projecty na stałe uruchomione. Happy path to po prostu request.json() i Response.json(), ale produkcyjna ścieżka wymaga czterech dodatkowych nawyków: zawijaj parsowanie body w try/catch, dodawaj nagłówki CORS z głową, sprawdzaj upstream.ok przed parsowaniem odpowiedzi z proxy i używaj ctx.waitUntil do zapisów do cache'a i innej pracy w tle. Zrób te cztery rzeczy dobrze, a będziesz wypuszczał Workery, które się trzymają.