Od nuly k dělníkovi ve výrobě

V předchozím článku jsme pochopili izoláty V8 a proč eliminují chlad začíná. V tomto článku budujeme něco konkrétního: Dělníka, který řídí směrování, čte proměnné prostředí, používá tajemství a odpovídá pomocí JSON. Na konci budete mít na svém účtu Cloudflare nasazeného pracovníka, který bude dostupný globálně za méně než 1 ms spuštění.

Co se naučíte

  • Nastavení vývojového prostředí pomocí Wrangler CLI a TypeScript
  • Struktura souboru wrangler.toml a všechny hlavní obory
  • Podrobně popisovač načtení: request, env, ExecutionContext
  • Ruční a itty-router směrování pro složité vzory adres URL
  • Proměnné prostředí, tajné klíče a jak k nim přistupovat v typově bezpečném TypeScriptu
  • Místní test s wrangler dev a znovu nabití za tepla
  • Nasazení do výroby a správy prostředí (staging/prod)
  • Vlastní domény a vzory cest

Předpoklady a počáteční nastavení

Budete potřebovat bezplatný účet Cloudflare a Node.js 18+. Volná úroveň zahrnuje 100 000 požadavků denně, což je více než dost pro vývoj a osobní projekty.

# 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

Vytvořte projekt Worker

Wrangler poskytuje příkaz create s kým projekt odkládá TypeScript a všechny potřebné závislosti:

# 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: Centrální konfigurace

Soubor wrangler.toml je srdcem konfigurace Worker. Každé pole má dopad na chování nasazení:

# 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"

The Fetch Handler: Kompletní anatomie

Každý pracovník musí exportovat výchozí objekt s metodou fetch. Podívejme se podrobně na každý parametr:

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

Pokročilé směrování s itty-routerem

U aplikací s mnoha cestami se psaní ručního směrování stává podrobným. Knihovna itty-router je navržen speciálně pro pracovníky: je malá písmena (méně než 1 kB) a používá stejná standardní webová rozhraní API.

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

Správa proměnných a tajemství prostředí

Workers rozlišuje dva typy konfiguračních hodnot: proměnné (viditelné v kódu a ve Wrangleru, nešifrováno) e tajemství (zašifrované, viditelné pouze v Workeru za běhu, nikdy nevymazané na řídicím panelu).

# 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

Proměnné vs tajemství: Kdy použít co

  • Proměnné ([vars]): Veřejné adresy URL API, příznaky funkcí, konfigurace životního prostředí. Nepoužívejte je pro citlivá data, protože jsou viditelné v wrangler.toml a v Cloudflare Dashboard.
  • tajemství: Klíče API, hesla k databázi, tokeny JWT, jakákoli data citlivý. Jsou zašifrované a nikdy nejsou odhaleny v čisté podobě, dokonce ani v protokolech.

Místní rozvoj s wrangler dev

wrangler dev spustí místní server, který simuluje běhové prostředí Workers automatické nabíjení za tepla:

# 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",...}

Pro místní tajné informace vytvořte soubor .dev.vars (bude přidáno .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

Nasadit ve výrobě

Deploy je jediný příkaz, který kompiluje TypeScript a sdružuje závislosti s esbuild a nahrajte výsledek do více než 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

Vlastní domény a vzory tras

Chcete-li pracovníka přidružit k vlastní doméně (která musí být na Cloudflare), nakonfigurujte trasy v 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)

Pomocníci pro správu chyb a odezvy

Robustní vzor zpracování chyb používá globální obal:

// 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 u pracovníků

Pro rozhraní API, která je třeba volat z prohlížečů v různých doménách, nakonfigurujte CORS přímo v Pracovníkovi:

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

Závěry a další kroky

Nyní máte všechny nástroje k vytvoření robustních TypeScript Worker: Routing with itty-router, správa proměnných a tajemství, místní vývoj s wrangler dev a nasadit do výroby. Dalším krokem je, aby vaše pracovníky byly stavové s různými vrstvami úložiště, které Cloudflare zpřístupňuje.

Další články v seriálu

  • Článek 3: Stálost hran — KV, R2 a D1 — kdy a jak používat Workers KV pro globální ukládání do mezipaměti, R2 pro ukládání objektů S3-kompatibilní bez poplatku za výstup a D1 pro relační SQL dotazy.
  • Článek 4: Trvanlivé předměty — primitivní stav silně konzistentní a WebSocket na hraně.
  • Článek 9: Testování s Miniflare a Vitest — jednotkové testy a integrační testy pro pracovníky bez nasazení.