Van nul tot arbeider in de productie

In het vorige artikel begrepen we V8-isolaten en waarom ze kou elimineren begint. In dit artikel bouwen we aan iets concreets: een Arbeider die leiding geeft routing, leest omgevingsvariabelen, gebruikt geheimen en reageert met JSON. Aan het einde Er wordt een Worker ingezet op uw Cloudflare-account, die wereldwijd bereikbaar is in minder dan 1 ms opstarten.

Wat je gaat leren

  • Ontwikkelomgeving instellen met Wrangler CLI en TypeScript
  • Bestandsstructuur wrangler.toml en alle belangrijke velden
  • De fetch-handler: request, env, ExecutionContext in detail
  • Handmatige en itty-routerroutering voor complexe URL-patronen
  • Omgevingsvariabelen, geheimen en hoe u deze kunt openen in typeveilige TypeScript
  • Lokale test met wrangler dev en heet herladen
  • Implementeren in productie- en omgevingsbeheer (staging/prod)
  • Aangepaste domeinen en routepatronen

Vereisten en initiële installatie

Je hebt een gratis Cloudflare-account en Node.js 18+ nodig. De vrije laag omvat 100.000 verzoeken per dag, meer dan genoeg voor ontwikkeling en persoonlijke projecten.

# Installa Wrangler globalmente (CLI ufficiale Cloudflare)
npm install -g wrangler

# Verifica l'installazione
wrangler --version
# wrangler 3.x.x

# Autenticati con il tuo account Cloudflare
# Apre il browser per il login OAuth
wrangler login

# Verifica che l'autenticazione abbia funzionato
wrangler whoami
# Logged in as: tuo@email.com
# Account ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Maak het werknemersproject

Wrangler geeft een commando create met wie het project op de plank ligt TypeScript en alle noodzakelijke afhankelijkheden:

# Crea un nuovo Worker con template TypeScript
npm create cloudflare@latest mio-primo-worker -- --type worker

# Oppure con Wrangler direttamente
wrangler init mio-primo-worker --type worker

# Struttura del progetto generata:
mio-primo-worker/
├── src/
│   └── index.ts          # Entry point del Worker
├── test/
│   └── index.spec.ts     # Test con Vitest (opzionale)
├── wrangler.toml          # Configurazione principale
├── tsconfig.json          # TypeScript config
├── package.json
└── .gitignore

cd mio-primo-worker
npm install

wrangler.toml: De centrale configuratie

Het bestand wrangler.toml het is het hart van de Worker-configuratie. Elk veld heeft gevolgen voor het implementatiegedrag:

# wrangler.toml

# Nome del Worker (deve essere unico nel tuo account)
name = "mio-primo-worker"

# Versione del formato di configurazione
main = "src/index.ts"

# Compatibilita: usa sempre una data recente per avere le ultime API
compatibility_date = "2025-11-01"

# Flags per abilitare funzionalita sperimentali/stabili
compatibility_flags = ["nodejs_compat"]

# Workers paused: false (default)
# workers_dev = true  # Abilita il sottodominio .workers.dev (default true)

# Variabili d'ambiente (visibili nel codice, NON secret)
[vars]
ENVIRONMENT = "production"
API_BASE_URL = "https://api.esempio.it"
MAX_RETRIES = "3"

# Route patterns: il Worker risponde a questi URL
# (richiede un dominio nel tuo account Cloudflare)
# [[routes]]
# pattern = "esempio.it/api/*"
# zone_name = "esempio.it"

# Ambiente di staging (sovrascrive i valori di root)
[env.staging]
name = "mio-primo-worker-staging"
vars = { ENVIRONMENT = "staging" }

# Binding KV Namespace (vedi articolo 3)
# [[kv_namespaces]]
# binding = "CACHE"
# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# preview_id = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"

De ophaalhandler: volledige anatomie

Elke Werker moet een standaardobject exporteren met een methode fetch. Laten we elke parameter in detail bekijken:

// src/index.ts

export interface Env {
  // Variabili definite in [vars] di wrangler.toml
  ENVIRONMENT: string;
  API_BASE_URL: string;
  MAX_RETRIES: string;

  // Secrets aggiunti con wrangler secret put
  DATABASE_URL: string;
  API_KEY: string;

  // Binding KV (se configurato)
  // CACHE: KVNamespace;
}

export default {
  /**
   * Fetch handler: chiamato per ogni richiesta HTTP
   *
   * @param request - La richiesta HTTP in arrivo (Request standard WhatWG)
   * @param env - I binding: variabili, secrets, KV, R2, D1, Durable Objects, AI
   * @param ctx - Execution context: waitUntil() per operazioni post-risposta
   */
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // Informazioni Cloudflare sulla richiesta
    const cf = request.cf;
    console.log({
      country: cf?.country,       // "IT", "US", "DE", ...
      city: cf?.city,             // "Milan", "New York", ...
      datacenter: cf?.colo,       // "MXP" (Milano), "LHR" (Londra), ...
      asn: cf?.asn,               // Numero ASN dell'ISP
      tlsVersion: cf?.tlsVersion, // "TLSv1.3"
    });

    const url = new URL(request.url);
    const { pathname, searchParams } = url;

    // Routing semplice basato sul pathname
    if (pathname === '/') {
      return handleRoot(request, env);
    }

    if (pathname.startsWith('/api/')) {
      return handleApi(request, env, ctx, pathname);
    }

    if (pathname === '/health') {
      return new Response(JSON.stringify({ status: 'ok', env: env.ENVIRONMENT }), {
        headers: { 'Content-Type': 'application/json' },
      });
    }

    return new Response('Not Found', {
      status: 404,
      headers: { 'Content-Type': 'text/plain' },
    });
  },
};

async function handleRoot(request: Request, env: Env): Promise<Response> {
  const html = `
    <!DOCTYPE html>
    <html>
      <head><title>Mio Primo Worker</title></head>
      <body>
        <h1>Hello from ${env.ENVIRONMENT}!</h1>
        <p>Cloudflare Worker is running.</p>
      </body>
    </html>
  `;
  return new Response(html, {
    headers: { 'Content-Type': 'text/html; charset=utf-8' },
  });
}

async function handleApi(
  request: Request,
  env: Env,
  ctx: ExecutionContext,
  pathname: string,
): Promise<Response> {
  // Verifica l'API key (dal header Authorization)
  const authHeader = request.headers.get('Authorization');
  if (!authHeader || authHeader !== `Bearer ${env.API_KEY}`) {
    return new Response(JSON.stringify({ error: 'Unauthorized' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  // Proxy verso il backend con retry logic
  const maxRetries = parseInt(env.MAX_RETRIES);
  const backendUrl = `${env.API_BASE_URL}${pathname}`;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(backendUrl, {
        method: request.method,
        headers: request.headers,
        body: request.method !== 'GET' ? request.body : undefined,
      });

      if (response.ok || attempt === maxRetries - 1) {
        return response;
      }
    } catch (error) {
      if (attempt === maxRetries - 1) {
        return new Response(JSON.stringify({ error: 'Backend unavailable' }), {
          status: 502,
          headers: { 'Content-Type': 'application/json' },
        });
      }
      // Exponential backoff (attenzione: consuma CPU time)
      await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 100));
    }
  }

  return new Response(JSON.stringify({ error: 'Max retries exceeded' }), { status: 502 });
}

Geavanceerde routering met itty-router

Voor toepassingen met veel routes wordt het schrijven van handmatige routes uitgebreid. De bibliotheek itty-router is speciaal ontworpen voor werknemers: dat is het ook kleine letters (minder dan 1 KB) en gebruikt dezelfde standaard web-API's.

# Installa itty-router
npm install itty-router
// src/index.ts con itty-router

import { Router, error, json, withParams } from 'itty-router';

export interface Env {
  ENVIRONMENT: string;
  API_KEY: string;
}

const router = Router();

// Middleware globale: aggiunge params al request object
router.all('*', withParams);

// Middleware di autenticazione
const authenticate = (request: Request & { params: Record<string, string> }, env: Env) => {
  const apiKey = request.headers.get('x-api-key');
  if (apiKey !== env.API_KEY) {
    return error(401, 'Invalid API key');
  }
  // Ritornare undefined continua al prossimo handler
};

// Route pubbliche
router.get('/', () => new Response('Hello from Workers!'));

router.get('/health', (req: Request, env: Env) =>
  json({ status: 'ok', environment: env.ENVIRONMENT, timestamp: Date.now() })
);

// Route con parametri URL
router.get('/users/:userId', authenticate, async (request, env: Env) => {
  const { userId } = request.params;

  // Fetch dall'origine con parametri
  const user = await fetchUser(userId, env);
  if (!user) {
    return error(404, `User ${userId} not found`);
  }

  return json(user);
});

// Route con query parameters
router.get('/search', async (request: Request) => {
  const url = new URL(request.url);
  const query = url.searchParams.get('q');
  const page = parseInt(url.searchParams.get('page') ?? '1');
  const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '10'), 100);

  if (!query) {
    return error(400, 'Query parameter "q" is required');
  }

  return json({
    query,
    page,
    limit,
    results: [], // qui andrebbero i risultati reali
  });
});

// POST con body JSON
router.post('/users', authenticate, async (request: Request, env: Env) => {
  let body: unknown;
  try {
    body = await request.json();
  } catch {
    return error(400, 'Invalid JSON body');
  }

  // Validazione base
  if (typeof body !== 'object' || body === null || !('name' in body)) {
    return error(422, 'Field "name" is required');
  }

  // Logica di business...
  return json({ created: true, data: body }, { status: 201 });
});

// Catch-all per 404
router.all('*', () => error(404, 'Route not found'));

async function fetchUser(userId: string, env: Env) {
  // Esempio: fetch da un'API esterna
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
  if (!response.ok) return null;
  return response.json();
}

export default {
  fetch: router.fetch,
};

Omgevingsvariabelen en geheimen beheren

Workers onderscheidt twee soorten configuratiewaarden: variabelen (zichtbaar in de code en in Wrangler, niet gecodeerd) e geheimen (gecodeerd, alleen zichtbaar in de Worker tijdens runtime, nooit duidelijk in het dashboard).

# Aggiungere un secret (interattivo: chiede il valore)
wrangler secret put DATABASE_URL
# Enter a secret value: [digitare il valore, non visibile]
# Successfully created secret DATABASE_URL

# Aggiungere un secret da stdin (utile in CI/CD)
echo "postgres://user:password@host:5432/db" | wrangler secret put DATABASE_URL

# Listare i secrets (mostra solo i nomi, non i valori)
wrangler secret list
# [
#   { "name": "DATABASE_URL", "type": "secret_text" },
#   { "name": "API_KEY", "type": "secret_text" }
# ]

# Eliminare un secret
wrangler secret delete DATABASE_URL

# Aggiungere secrets per ambiente specifico
wrangler secret put DATABASE_URL --env staging
wrangler secret put DATABASE_URL --env production

Variabelen versus geheimen: wanneer wat te gebruiken

  • Variabelen ([vars]): Openbare API-URL's, functievlaggen, configuratie van het milieu. Gebruik ze niet voor gevoelige gegevens, omdat ze zichtbaar zijn in wrangler.toml en in het Cloudflare Dashboard.
  • Geheimen: API-sleutels, databasewachtwoorden, JWT-tokens, alle gegevens gevoelig. Ze zijn gecodeerd en worden nooit openbaar gemaakt, zelfs niet in de logbestanden.

Lokale ontwikkeling met wrangler-ontwikkelaar

wrangler dev start een lokale server waarmee de Workers-runtime wordt gesimuleerd automatisch warm herladen:

# Avvia il server di sviluppo locale
npm run dev  # equivale a: wrangler dev

# Output tipico:
# ⛅️ wrangler 3.x.x
# -------------------
# Using vars defined in .dev.vars
# Your Worker has access to the following bindings:
# - Vars:
#   - ENVIRONMENT: "development"
#   - API_BASE_URL: "https://api.esempio.it"
# ⎔ Starting local server...
# [wrangler:inf] Ready on http://localhost:8787

# In un altro terminale, testa le route:
curl http://localhost:8787/health
# {"status":"ok","environment":"development","timestamp":1742000000000}

curl -H "x-api-key: test-key" http://localhost:8787/users/1
# {"id":1,"name":"Leanne Graham","username":"Bret",...}

Maak voor lokale geheimen een bestand .dev.vars (wordt toegevoegd .gitignore!):

# .dev.vars - NON committare questo file!
# Questi valori sovrascrivono [vars] in wrangler.toml durante lo sviluppo locale

DATABASE_URL=postgres://localhost:5432/dev_db
API_KEY=dev-test-key-12345
STRIPE_WEBHOOK_SECRET=whsec_test_xxxxx

# Per usare .dev.vars anche per variabili non-secret:
ENVIRONMENT=development
API_BASE_URL=http://localhost:3000

Implementeren in productie

Deploy is een enkele opdracht die het TypeScript compileert en de afhankelijkheden bundelt met esbuild en upload het resultaat naar de 300+ Cloudflare PoPs:

# Deploy all'ambiente di default (production)
wrangler deploy

# Output:
# ⛅️ wrangler 3.x.x
# -------------------
# Total Upload: 45.32 KiB / gzip: 12.18 KiB
# Uploaded mio-primo-worker (1.23 sec)
# Published mio-primo-worker (0.45 sec)
#   https://mio-primo-worker.tuo-account.workers.dev
# Current Deployment ID: xxxxxxxxxx
# Current Version ID: yyyyyyyyyy

# Deploy su ambiente specifico
wrangler deploy --env staging

# Preview del deploy senza pubblicare
wrangler deploy --dry-run

# Verificare il Worker deployato
curl https://mio-primo-worker.tuo-account.workers.dev/health

Aangepaste domeinen en routepatronen

Om de Worker te koppelen aan een aangepast domein (dat zich op Cloudflare moet bevinden), configureert u de trajecten binnen wrangler.toml:

# wrangler.toml - configurazione route con dominio custom

name = "api-gateway"
main = "src/index.ts"
compatibility_date = "2025-11-01"

# Opzione 1: route pattern (piu flessibile)
# Corrisponde a: esempio.it/api/ e tutti i sottopercorsi
[[routes]]
pattern = "esempio.it/api/*"
zone_name = "esempio.it"

# Opzione 2: custom domain diretto (Workers For Platforms)
# [env.production]
# routes = [{ pattern = "api.esempio.it/*", zone_name = "esempio.it" }]

# Opzione 3: Worker Routes nel dashboard Cloudflare
# (utile per pattern complessi o gestione manuale)

Foutbeheer en responshelpers

Een robuust foutafhandelingspatroon maakt gebruik van een globale wrapper:

// src/utils/error-handler.ts

export function handleError(error: unknown): Response {
  if (error instanceof WorkerError) {
    return new Response(JSON.stringify({
      error: error.message,
      code: error.code,
    }), {
      status: error.status,
      headers: { 'Content-Type': 'application/json' },
    });
  }

  // Errore non atteso: non esporre dettagli in produzione
  console.error('Unexpected error:', error);
  return new Response(JSON.stringify({ error: 'Internal server error' }), {
    status: 500,
    headers: { 'Content-Type': 'application/json' },
  });
}

export class WorkerError extends Error {
  constructor(
    message: string,
    public readonly status: number = 500,
    public readonly code: string = 'INTERNAL_ERROR'
  ) {
    super(message);
    this.name = 'WorkerError';
  }

  static notFound(resource: string): WorkerError {
    return new WorkerError(`${resource} not found`, 404, 'NOT_FOUND');
  }

  static unauthorized(): WorkerError {
    return new WorkerError('Unauthorized', 401, 'UNAUTHORIZED');
  }

  static badRequest(message: string): WorkerError {
    return new WorkerError(message, 400, 'BAD_REQUEST');
  }
}

// src/index.ts - integrazione con il fetch handler
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    try {
      return await handleRequest(request, env, ctx);
    } catch (error) {
      return handleError(error);
    }
  },
};

CORS: Cross-Origin Management bij werknemers

Voor API's die moeten worden aangeroepen vanuit browsers op verschillende domeinen, configureert u CORS rechtstreeks in de Werknemer:

// src/middleware/cors.ts

const ALLOWED_ORIGINS = [
  'https://esempio.it',
  'https://www.esempio.it',
  'http://localhost:3000',  // solo in development
];

export function corsHeaders(request: Request, origin?: string): HeadersInit {
  const requestOrigin = origin ?? request.headers.get('Origin') ?? '';
  const allowed = ALLOWED_ORIGINS.includes(requestOrigin) ? requestOrigin : '';

  return {
    'Access-Control-Allow-Origin': allowed,
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization, x-api-key',
    'Access-Control-Max-Age': '86400',
    'Vary': 'Origin',
  };
}

// Gestisci il preflight OPTIONS
export function handleCors(request: Request): Response | null {
  if (request.method === 'OPTIONS') {
    return new Response(null, {
      status: 204,
      headers: corsHeaders(request),
    });
  }
  return null;  // Continua con il normale handling
}

// Integrazione nel fetch handler
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const corsResponse = handleCors(request);
    if (corsResponse) return corsResponse;

    const response = await router.fetch(request, env);

    // Aggiungi headers CORS a tutte le risposte
    const newResponse = new Response(response.body, response);
    Object.entries(corsHeaders(request)).forEach(([key, value]) => {
      newResponse.headers.set(key, value);
    });

    return newResponse;
  },
};

Conclusies en volgende stappen

U beschikt nu over alle tools om robuuste TypeScript Workers te creëren: Routing itty-router, beheer van variabelen en geheimen, lokale ontwikkeling met wrangler dev en inzetten in productie. De volgende stap is om uw werknemers stateful te maken met de verschillende opslaglagen die Cloudflare beschikbaar stelt.

Volgende artikelen in de serie

  • Artikel 3: Randpersistentie — KV, R2 en D1 — wanneer en hoe Workers KV te gebruiken voor globale caching, R2 voor objectopslag S3-compatibel zonder uitgaande kosten, en D1 voor relationele SQL-query's.
  • Artikel 4: Duurzame objecten – de primitieve staat sterk consistent en WebSocket aan de rand.
  • Artikel 9: Testen met Miniflare en Vitest – eenheidstesten en integratietests voor werknemers zonder inzet.