Caching aan de rand: verder dan traditioneel CDN

Een traditioneel CDN slaat statische assets op in de cache volgens standaard HTTP-headers. Cloudflare-werknemers gaat nog veel verder met het concept: met de Cache-API, jouw code TypeScript bepaalt met chirurgische precisie wat er in de cache wordt opgeslagen, hoe lang, hoe het ongeldig wordt gemaakt en welke fallback-logica moet worden toegepast als de cache verouderd is.

Het resultaat is één Programmeerbaar CDN: in plaats van regels te configureren statisch in een dashboard, schrijf bedrijfslogica die direct beslist of een het antwoord is cachebaar, met welke TTL en met welke cachesleutel. Deze flexibiliteit het is vooral waardevol voor REST API's, aangepaste pagina's en semi-dynamische inhoud.

Wat je gaat leren

  • Hoe de Cloudflare Workers Cache API werkt en hoe deze verschilt van de Browser Cache API
  • Cachingstrategieën: Cache-First, Network-First, Stale-While-Revalidate
  • Aangepaste cachesleutels: isoleer de cache op gebruiker, taal en versie
  • Ongeldigheid voor URL's, tags en voorvoegsels met de Cloudflare Zone Purge API
  • Varieer headers: gedifferentieerde cache voor Accept-Encoding, Accept-Language
  • Geavanceerde patronen: cache-opwarming, respijtperiode en stroomonderbreker met KV
  • Veel voorkomende fouten en hoe u deze tijdens de productie kunt vermijden

De Cache API: basisprincipes en verschillen met de browser

De Cache-API die wordt weergegeven in Workers volgt dezelfde interface als de Cache-API voor servicemedewerkers browser, maar met belangrijke verschillen. In Workers is de cache gedistribueerd op alle Cloudflare PoP's: Wanneer een medewerker in Frankfurt een antwoord in de cache opslaat, dat antwoord is niet automatisch beschikbaar in Londen of Amsterdam. Elke PoP heeft de eigen lokale cache.

// Accesso alla cache nel Worker
// In Workers esiste un unico "default" cache namespace
const cache = caches.default;

// Oppure cache named (isolata per nome, utile per namespace logici)
const apiCache = await caches.open('api-v2');

// Le operazioni fondamentali:
// cache.match(request) -> Response | undefined
// cache.put(request, response) -> void
// cache.delete(request) -> boolean

Cache-API: belangrijke beperkingen

  • Alleen HTTP-verzoeken (geen willekeurige URL's zoals in de browser)
  • Het is niet mogelijk om antwoorden te cachen met Vary: *
  • De cache is per-PoP: er vindt geen automatische cross-datacenter-invalidatie plaats cache.delete()
  • Antwoorden met status 206 (Gedeeltelijke inhoud) kunnen niet in de cache worden opgeslagen
  • De maximaal gerespecteerde TTL is 31 dagen

Strategie 1: Cache-First met Fallback Network

De meest gebruikelijke strategie voor API's met zelden veranderende gegevens: serveren vanuit de cache indien beschikbaar, ga anders naar de bron en vul de cache.

// worker.ts - Cache-First Strategy

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // Solo richieste GET sono cacheable
    if (request.method !== 'GET') {
      return fetch(request);
    }

    const cache = caches.default;

    // 1. Controlla la cache
    let response = await cache.match(request);

    if (response) {
      // Cache HIT: aggiungi header diagnostico e restituisci
      const headers = new Headers(response.headers);
      headers.set('X-Cache-Status', 'HIT');
      return new Response(response.body, {
        status: response.status,
        headers,
      });
    }

    // 2. Cache MISS: fetch dall'origin
    response = await fetch(request);

    // 3. Clona la risposta (il body e un ReadableStream, consumabile una sola volta)
    const responseToCache = response.clone();

    // 4. Metti in cache con ctx.waitUntil() per non bloccare la risposta al client
    ctx.waitUntil(
      cache.put(request, responseToCache)
    );

    // 5. Restituisci la risposta originale con header diagnostico
    const headers = new Headers(response.headers);
    headers.set('X-Cache-Status', 'MISS');
    return new Response(response.body, {
      status: response.status,
      headers,
    });
  },
};

interface Env {}

Strategie 2: Verouderd, terwijl, opnieuw valideren

De strategie oud-terwijl-revalideren het is degene die het beste geeft compromis tussen dataversheid en waargenomen snelheid: de klant ontvangt Altijd een onmiddellijke reactie van de cache, zelfs als deze op de achtergrond draait de werknemer werkt de cache bij voor het volgende verzoek.

// Stale-While-Revalidate implementato manualmente
// (Cloudflare supporta anche il header standard, ma questa versione offre più controllo)

const CACHE_TTL = 60;        // Secondi prima che la cache sia "fresh"
const STALE_TTL = 300;       // Secondi aggiuntivi in cui la cache e "stale but usable"

interface CacheMetadata {
  cachedAt: number;
  ttl: number;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    if (request.method !== 'GET') return fetch(request);

    const cache = caches.default;

    // Crea una Request con una custom cache key che include i metadata
    const cacheKey = new Request(request.url, {
      headers: request.headers,
    });

    const cached = await cache.match(cacheKey);

    if (cached) {
      const cachedAt = parseInt(cached.headers.get('X-Cached-At') ?? '0');
      const age = (Date.now() - cachedAt) / 1000;

      if (age < CACHE_TTL) {
        // FRESH: servi dalla cache senza revalidazione
        return addCacheHeaders(cached, 'FRESH', age);
      }

      if (age < CACHE_TTL + STALE_TTL) {
        // STALE: servi dalla cache ma revalida in background
        ctx.waitUntil(revalidate(cacheKey, cache));
        return addCacheHeaders(cached, 'STALE', age);
      }
    }

    // MISS o troppo vecchio: fetch sincrono
    return fetchAndCache(cacheKey, cache, ctx);
  },
};

async function revalidate(cacheKey: Request, cache: Cache): Promise<void> {
  const fresh = await fetch(cacheKey.url);
  if (fresh.ok) {
    const toCache = addTimestamp(fresh);
    await cache.put(cacheKey, toCache);
  }
}

async function fetchAndCache(
  cacheKey: Request,
  cache: Cache,
  ctx: ExecutionContext
): Promise<Response> {
  const response = await fetch(cacheKey.url);

  if (response.ok) {
    const toCache = addTimestamp(response.clone());
    ctx.waitUntil(cache.put(cacheKey, toCache));
  }

  const headers = new Headers(response.headers);
  headers.set('X-Cache-Status', 'MISS');
  return new Response(response.body, { status: response.status, headers });
}

function addTimestamp(response: Response): Response {
  const headers = new Headers(response.headers);
  headers.set('X-Cached-At', String(Date.now()));
  // Cache-Control: max-age elevato per far "sopravvivere" la risposta in cache
  headers.set('Cache-Control', 'public, max-age=86400');
  return new Response(response.body, { status: response.status, headers });
}

function addCacheHeaders(response: Response, status: string, age: number): Response {
  const headers = new Headers(response.headers);
  headers.set('X-Cache-Status', status);
  headers.set('Age', String(Math.floor(age)));
  return new Response(response.body, { status: response.status, headers });
}

interface Env {}

Aangepaste cachesleutels: Isoleer cache op basis van context

Standaard is de cachesleutel de volledige URL van het verzoek. Maar vaak heb je dat nodig van caches gescheiden door taal, API-versie, gebruikers- of apparaatlaag. De oplossing is bouw er een aangepaste cachesleutel als voorwerp Request met een korte URL.

// Cache differenziata per lingua e versione API

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const cache = caches.default;
    const url = new URL(request.url);

    // Estrai parametri rilevanti per la cache key
    const lang = request.headers.get('Accept-Language')?.split(',')[0]?.split('-')[0] ?? 'en';
    const apiVersion = url.searchParams.get('v') ?? 'v1';
    const tier = request.headers.get('X-User-Tier') ?? 'free';

    // Costruisci una URL sintetica come cache key
    // Non deve essere una URL reale, solo identificativa
    const cacheKeyUrl = new URL(request.url);
    cacheKeyUrl.searchParams.set('_ck_lang', lang);
    cacheKeyUrl.searchParams.set('_ck_v', apiVersion);
    // Non includiamo 'tier' nella key se vuoi condividere la cache tra tier

    const cacheKey = new Request(cacheKeyUrl.toString(), {
      method: 'GET',
      // Importante: non copiare headers di autenticazione nella cache key
      // altrimenti ogni utente avrebbe la propria cache entry
    });

    // Cerca nella cache con la custom key
    let response = await cache.match(cacheKey);

    if (response) {
      return response;
    }

    // Fetch dall'origin passando la richiesta originale (con auth headers)
    const originResponse = await fetch(request);

    if (originResponse.ok && isCacheable(originResponse)) {
      const toCache = setCacheHeaders(originResponse.clone(), 300);
      ctx.waitUntil(cache.put(cacheKey, toCache));
    }

    return originResponse;
  },
};

function isCacheable(response: Response): boolean {
  // Non mettere in cache risposte con dati personali o Set-Cookie
  if (response.headers.has('Set-Cookie')) return false;
  if (response.headers.get('Cache-Control')?.includes('private')) return false;
  if (response.headers.get('Cache-Control')?.includes('no-store')) return false;
  return true;
}

function setCacheHeaders(response: Response, maxAge: number): Response {
  const headers = new Headers(response.headers);
  headers.set('Cache-Control', `public, max-age=${maxAge}, s-maxage=${maxAge}`);
  // Rimuovi header che potrebbero impedire il caching
  headers.delete('Set-Cookie');
  return new Response(response.body, { status: response.status, headers });
}

interface Env {}

Cacheheaders: s-maxage, oud-terwijl-revalidate, oud-als-error

Cloudflare respecteert de Standaard Cache-Control-headers en verlengt deze de betekenis. Het begrijpen van deze richtlijnen is essentieel:

Richtlijn Betekenis Voorbeeld
max-age=N TTL voor browser en CDN (N seconden) max-age=300
s-maxage=N TTL alleen voor CDN/proxy (overschrijft de maximale leeftijd voor Cloudflare) s-maxage=3600, max-age=60
stale-while-revalidate=N Extra seconden om muf te serveren terwijl het revalideert s-maxage=60, stale-while-revalidate=300
stale-if-error=N Seconden waarin moet worden geserveerd als de oorsprong een fout retourneert stale-if-error=86400
no-store Cache onder geen enkele omstandigheid Voor gevoelige gegevens
private Alleen browser in cache, niet CDN Voor geverifieerde antwoorden
// Esempio: API con s-maxage e stale-while-revalidate via header

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    // Routing con TTL differenziati per tipo di risorsa
    if (url.pathname.startsWith('/api/products')) {
      return fetchWithCacheHeaders(request, {
        sMaxAge: 300,           // 5 minuti fresh in CDN
        staleWhileRevalidate: 3600, // 1 ora stale accettabile
        staleIfError: 86400,    // 1 giorno stale in caso di errore origin
      });
    }

    if (url.pathname.startsWith('/api/user')) {
      // Dati utente: non cacheare in CDN
      return fetchWithCacheHeaders(request, {
        sMaxAge: 0,
        private: true,
      });
    }

    if (url.pathname.startsWith('/static')) {
      // Asset statici: cache aggressiva
      return fetchWithCacheHeaders(request, {
        sMaxAge: 31536000, // 1 anno
        immutable: true,
      });
    }

    return fetch(request);
  },
};

interface CacheOptions {
  sMaxAge?: number;
  staleWhileRevalidate?: number;
  staleIfError?: number;
  private?: boolean;
  immutable?: boolean;
}

async function fetchWithCacheHeaders(
  request: Request,
  options: CacheOptions
): Promise<Response> {
  const response = await fetch(request);
  const headers = new Headers(response.headers);

  let cacheControl = '';

  if (options.private) {
    cacheControl = 'private, no-store';
  } else {
    const parts: string[] = ['public'];
    if (options.sMaxAge !== undefined) parts.push(`s-maxage=${options.sMaxAge}`);
    if (options.staleWhileRevalidate) parts.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
    if (options.staleIfError) parts.push(`stale-if-error=${options.staleIfError}`);
    if (options.immutable) parts.push('immutable');
    cacheControl = parts.join(', ');
  }

  headers.set('Cache-Control', cacheControl);
  return new Response(response.body, { status: response.status, headers });
}

interface Env {}

Ongeldigheid: opschonen van URL, tag en voorvoegsel

cache.delete(request) verwijder de cache alleen in de lokale PoP waar de Worker draait. Om de cache ongeldig te maken voor alle PoP's wereldwijd, moet je de API gebruiken Cloudflare Zone Opschonen REST. Dit is het juiste mechanisme voor contentbeheer en inzetten.

// Invalidation globale tramite Cloudflare API
// Da usare tipicamente da un webhook CMS o da un Worker admin

interface PurgeOptions {
  files?: string[];      // URL specifici
  tags?: string[];       // Cache-Tag headers
  prefixes?: string[];   // URL prefix
  hosts?: string[];      // Tutti gli URL di un host
}

async function purgeCloudflareCache(
  zoneId: string,
  apiToken: string,
  options: PurgeOptions
): Promise<void> {
  const response = await fetch(
    `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiToken}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(options),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`Purge failed: ${JSON.stringify(error)}`);
  }
}

// Worker che funge da webhook per invalidazione CMS
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    if (request.method !== 'POST') {
      return new Response('Method Not Allowed', { status: 405 });
    }

    // Verifica il secret del webhook
    const secret = request.headers.get('X-Webhook-Secret');
    if (secret !== env.WEBHOOK_SECRET) {
      return new Response('Unauthorized', { status: 401 });
    }

    const body = await request.json() as WebhookPayload;

    // Invalida le URL specifiche aggiornate dal CMS
    if (body.type === 'post.updated') {
      await purgeCloudflareCache(env.ZONE_ID, env.CF_API_TOKEN, {
        files: [
          `https://example.com/blog/${body.slug}`,
          `https://example.com/api/posts/${body.id}`,
          `https://example.com/sitemap.xml`,
        ],
      });
    }

    // Invalida per tag (richiede Cache-Tag header sulle risposte origin)
    if (body.type === 'category.updated') {
      await purgeCloudflareCache(env.ZONE_ID, env.CF_API_TOKEN, {
        tags: [`category-${body.categorySlug}`],
      });
    }

    return new Response(JSON.stringify({ purged: true }), {
      headers: { 'Content-Type': 'application/json' },
    });
  },
};

interface WebhookPayload {
  type: string;
  id?: string;
  slug?: string;
  categorySlug?: string;
}

interface Env {
  ZONE_ID: string;
  CF_API_TOKEN: string;
  WEBHOOK_SECRET: string;
}

Cachetags: semantische ongeldigverklaring

I Cachetags ze zijn het krachtigste mechanisme voor invalidatie selectief. Ze werken door een header toe te voegen Cache-Tag naar de antwoorden: elk antwoord kan meerdere tags hebben en u kunt alle URL's ongeldig maken gekoppeld aan een tag met een enkele API-aanroep.

// Origin server o Worker che aggiunge Cache-Tag alle risposte

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const response = await fetch(request);
    const headers = new Headers(response.headers);

    // Aggiungi tag semantici basati sul contenuto
    const tags: string[] = [];

    // Tag per tipo di risorsa
    if (url.pathname.startsWith('/api/products')) {
      const productId = url.pathname.split('/')[3];
      tags.push('products');                    // Invalida tutti i prodotti
      if (productId) tags.push(`product-${productId}`); // Invalida questo prodotto specifico
    }

    if (url.pathname.startsWith('/api/categories')) {
      const catId = url.pathname.split('/')[3];
      tags.push('categories');
      if (catId) tags.push(`category-${catId}`);
    }

    // Tag per versione dell'API
    const apiVersion = url.pathname.split('/')[2];
    if (apiVersion?.startsWith('v')) {
      tags.push(`api-${apiVersion}`);
    }

    if (tags.length > 0) {
      // Cache-Tag: lista separata da virgole, max 16KB
      headers.set('Cache-Tag', tags.join(','));
    }

    return new Response(response.body, { status: response.status, headers });
  },
};

// Esempio di invalidazione per tag dopo un aggiornamento:
// POST /api/admin/purge
// { "tags": ["product-123", "categories"] }
// Invalida tutte le URL che hanno Cache-Tag: product-123 o categories

interface Env {}

Geavanceerd patroon: cache met KV als L2

De Cache API heeft een belangrijke beperking: deze is niet programmatisch toegankelijk voor willekeurig lezen/schrijven, alleen HTTP-verzoeken. Voor meer patronen complex (zoals gecoördineerde invalidatie, stroomonderbreker of objectcache niet-HTTP), gebruik Werknemers KV als L2-cache.

// Cache a due livelli: Cache API (L1, HTTP) + KV (L2, programmabile)

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const cacheKey = buildCacheKey(url);

    // L1: Cache API (piu veloce, locale al PoP)
    const l1Cache = caches.default;
    const l1Hit = await l1Cache.match(request);

    if (l1Hit) {
      return addHeader(l1Hit, 'X-Cache', 'L1-HIT');
    }

    // L2: KV Store (globale, programmabile, con TTL gestito da KV)
    const kvCached = await env.API_CACHE.getWithMetadata<CacheMetadata>(cacheKey);

    if (kvCached.value !== null) {
      const { value, metadata } = kvCached;

      // Ricostruisci una Response dalla stringa KV
      const cachedResponse = new Response(value, {
        headers: {
          'Content-Type': metadata?.contentType ?? 'application/json',
          'Cache-Control': 'public, max-age=60',
          'X-Cache': 'L2-HIT',
          'X-Cached-At': String(metadata?.cachedAt ?? 0),
        },
      });

      // Popola anche L1 per richieste successive nello stesso PoP
      ctx.waitUntil(l1Cache.put(request, cachedResponse.clone()));

      return cachedResponse;
    }

    // MISS su entrambi i livelli: fetch dall'origin
    const originResponse = await fetch(request);

    if (originResponse.ok) {
      const body = await originResponse.text();
      const contentType = originResponse.headers.get('Content-Type') ?? 'application/json';

      const metadata: CacheMetadata = {
        cachedAt: Date.now(),
        contentType,
        url: url.toString(),
      };

      // Salva in KV con TTL di 5 minuti
      ctx.waitUntil(
        env.API_CACHE.put(cacheKey, body, {
          expirationTtl: 300,
          metadata,
        })
      );

      // Salva anche in L1
      const toL1 = new Response(body, {
        headers: {
          'Content-Type': contentType,
          'Cache-Control': 'public, max-age=60',
          'X-Cache': 'MISS',
        },
      });
      ctx.waitUntil(l1Cache.put(request, toL1));

      return new Response(body, {
        headers: {
          'Content-Type': contentType,
          'X-Cache': 'MISS',
        },
      });
    }

    return addHeader(originResponse, 'X-Cache', 'BYPASS-ERROR');
  },
};

function buildCacheKey(url: URL): string {
  // Normalizza l'URL per la cache key (rimuovi query params non semantici)
  const params = new URLSearchParams(url.searchParams);
  params.delete('utm_source');
  params.delete('utm_medium');
  params.delete('_t'); // timestamp di cache-busting
  params.sort(); // ordine deterministico
  return `${url.pathname}?${params.toString()}`;
}

function addHeader(response: Response, key: string, value: string): Response {
  const headers = new Headers(response.headers);
  headers.set(key, value);
  return new Response(response.body, { status: response.status, headers });
}

interface CacheMetadata {
  cachedAt: number;
  contentType: string;
  url: string;
}

interface Env {
  API_CACHE: KVNamespace;
}

Cache-opwarming: vul de cache vooraf in

Il cache-opwarming het is de gewoonte om eerst de cache vooraf in te vullen dat er echte verzoeken binnenkomen, waardoor het probleem met de koude cache na de implementatie wordt geëlimineerd enorme ontkenningen. Het wordt geïmplementeerd met een Worker die is gepland via Cron Trigger.

// wrangler.toml - Cron Trigger per cache warming
// [triggers]
// crons = ["*/15 * * * *"]  # Ogni 15 minuti

// worker.ts - Cache Warming Worker

const URLS_TO_WARM = [
  'https://api.example.com/products?featured=true',
  'https://api.example.com/categories',
  'https://api.example.com/homepage',
  'https://api.example.com/navigation',
];

export default {
  // Scheduled handler per Cron Trigger
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
    console.log(`Cache warming started at ${new Date(event.scheduledTime).toISOString()}`);

    const results = await Promise.allSettled(
      URLS_TO_WARM.map(url => warmUrl(url))
    );

    const succeeded = results.filter(r => r.status === 'fulfilled').length;
    const failed = results.filter(r => r.status === 'rejected').length;

    console.log(`Cache warming complete: ${succeeded} success, ${failed} failed`);
  },

  async fetch(request: Request): Promise<Response> {
    return new Response('Cache Warmer Worker', { status: 200 });
  },
};

async function warmUrl(url: string): Promise<void> {
  // Forza bypass della cache aggiungendo header speciale
  // (da gestire lato Worker principale con whitelist IP o secret)
  const response = await fetch(url, {
    headers: {
      'Cache-Control': 'no-cache', // Forza revalidazione
      'X-Cache-Warm': 'true',
    },
  });

  if (!response.ok) {
    throw new Error(`Failed to warm ${url}: ${response.status}`);
  }
}

interface Env {}
interface ScheduledEvent {
  scheduledTime: number;
  cron: string;
}

Cache-foutopsporing en -bewaking

Cloudflare maakt diagnostische headers zichtbaar in reacties om de cachestatus te begrijpen. Het belangrijkste is CF-Cache-Status:

CF-cache-status Betekenis
HIT Gediend vanuit Cloudflare-cache
MISS Niet in de cache opgeslagen, opgevraagd bij de oorsprong
EXPIRED In cache opgeslagen maar TTL verlopen, verzoek aan oorsprong
STALE Oud geserveerd (oud-terwijl-revalideren)
BYPASS Cache omzeild (cookie, auth-header, etc.)
DYNAMIC Niet-cachebaar (dynamische respons)
REVALIDATED Cache gevalideerd met oorsprong (304 niet gewijzigd)
// Worker che logga le metriche di cache su KV Analytics
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const response = await fetch(request);

    const cacheStatus = response.headers.get('CF-Cache-Status') ?? 'UNKNOWN';

    // Logga la metrica in background
    ctx.waitUntil(
      logCacheMetric(env, {
        url: request.url,
        status: cacheStatus,
        timestamp: Date.now(),
        country: request.cf?.country ?? 'unknown',
        datacenter: request.cf?.colo ?? 'unknown',
      })
    );

    return response;
  },
};

async function logCacheMetric(env: Env, metric: CacheMetric): Promise<void> {
  // Aggrega per finestre di 1 minuto
  const minuteKey = `metrics:${Math.floor(metric.timestamp / 60000)}:${metric.status}`;
  const current = parseInt(await env.METRICS_KV.get(minuteKey) ?? '0');
  await env.METRICS_KV.put(minuteKey, String(current + 1), { expirationTtl: 3600 });
}

interface CacheMetric {
  url: string;
  status: string;
  timestamp: number;
  country: string;
  datacenter: string;
}

interface Env {
  METRICS_KV: KVNamespace;
}

Conclusies en volgende stappen

De Cloudflare Workers Cache API transformeert het CDN van een passieve tool naar een component actief in de architectuur. Met de strategieën die in dit artikel worden beschreven, kun je bouwen een granulaire, semantische, ongeldige cachinglaag die de belasting vermindert 70-90% op oorsprong voor typische openbare API-workloads.

De belangrijkste punten om te onthouden: gebruik ctx.waitUntil() om de reactie niet te blokkeren voor de client, bouw aangepaste cachesleutels om verschillende contexten te isoleren, gebruik cachetags voor semantische invalidatie, en combineer Cache API met KV voor complexere patronen.

Volgende artikelen in de serie

  • Artikel 9: Testen van werknemers op lokaal niveau — Miniflare, Vitest e Wrangler Dev: unit-tests en integratietests schrijven voor werknemers zonder implementatie.
  • Artikel 10: Full-stack architecturen aan de edge — Casestudy door Zero to Production: een complete REST API met Workers + D1 + R2 en CI/CD op GitHub-acties.