Salta al contenuto principale

02 - Il Tuo Primo Cloudflare Worker: Fetch Handler, Wrangler e Deploy

Edge Computing 12 min Il Tuo Primo Cloudflare Worker: Fetch Handler, Wrangler e Deploy Una guida step-by-step per creare, testare e deployare un...

Cosa ne pensi di questo articolo?

02 - Il Tuo Primo Cloudflare Worker: Fetch Handler, Wrangler e Deploy
Edge ComputingFoto:Brett SaylesPexels

In Breve

Edge Computing 12 min Il Tuo Primo Cloudflare Worker: Fetch Handler, Wrangler e Deploy Una guida step-by-step per creare, testare e deployare un...

Edge Computing 12 min

Il Tuo Primo Cloudflare Worker: Fetch Handler, Wrangler e Deploy

Una guida step-by-step per creare, testare e deployare un Cloudflare Worker da zero: dal fetch handler al wrangler deploy, passando per variabili d'ambiente, secrets e routing avanzato.

Cloudflare Workers Wrangler Tutorial Deploy Edge Functions

Da Zero a Worker in Produzione

Nell'articolo precedente abbiamo capito i V8 isolates e perché eliminano i cold starts. In questo articolo costruiamo qualcosa di concreto: un Worker che gestisce routing, legge variabili d'ambiente, usa secrets e risponde con JSON. Al termine avrai un Worker deployato sul tuo account Cloudflare, raggiungibile globalmente in meno di 1ms di startup.

Cosa Imparerai

  • Setup dell'ambiente di sviluppo con Wrangler CLI e TypeScript
  • Struttura del file wrangler.toml e tutti i campi principali
  • Il fetch handler: request, env, ExecutionContext in dettaglio
  • Routing manuale e con itty-router per URL patterns complessi
  • Variabili d'ambiente, secrets e come accederli in TypeScript type-safe
  • Test locale con wrangler dev e hot reload
  • Deploy in produzione e gestione ambienti (staging/prod)
  • Custom domains e route patterns

Prerequisiti e Setup Iniziale

Avrai bisogno di un account Cloudflare gratuito e di Node.js 18+. Il free tier include 100.000 richieste al giorno, più che sufficiente per sviluppo e progetti personali.

# 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

Creare il Progetto Worker

Wrangler fornisce un comando create che scaffola il progetto con TypeScript e tutte le dipendenze necessarie:

# 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: La Configurazione Centrale

Il file wrangler.toml è il cuore della configurazione del Worker. Ogni campo ha implicazioni sul comportamento in deploy:

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

Il Fetch Handler: Anatomia Completa

Ogni Worker deve esportare un oggetto default con un metodo fetch. Vediamo ogni parametro in dettaglio:

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

Routing Avanzato con itty-router

Per applicazioni con molte route, scrivere routing manuale diventa verboso. La libreria itty-router è progettata specificamente per Workers: è minuscola (meno di 1KB) e usa le stesse API Web standard.

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

Gestire Variabili d'Ambiente e Secrets

Workers distingue due tipi di valori di configurazione: variabili (visibili nel codice e in Wrangler, non cifrate) e secrets (cifrati, visibili solo nel Worker a runtime, mai in chiaro nel 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

Variabili vs Secrets: Quando Usare Cosa

  • Variabili ([vars]): URL di API pubbliche, feature flags, configurazione dell'ambiente. Non usarle per dati sensibili perché sono visibili in wrangler.toml e nel Dashboard Cloudflare.
  • Secrets: Chiavi API, password di database, token JWT, qualsiasi dato sensibile. Vengono cifrati e non sono mai esposti in chiaro, nemmeno nei log.

Sviluppo Locale con wrangler dev

wrangler dev avvia un server locale che simula il runtime di Workers con hot reload automatico:

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

Per i secrets in locale, crea un file .dev.vars (da aggiungere a .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

Deploy in Produzione

Il deploy è un singolo comando che compila il TypeScript, bundla le dipendenze con esbuild e carica il risultato sui 300+ PoP di Cloudflare:

# 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

Custom Domains e Route Patterns

Per associare il Worker a un dominio custom (che deve essere su Cloudflare), configura le route in 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)

Gestione degli Errori e Response Helpers

Un pattern robusto per la gestione degli errori usa un wrapper globale:

// 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: Gestione Cross-Origin in Workers

Per API che devono essere chiamate da browser su domini diversi, configura CORS direttamente nel Worker:

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

Conclusioni e Prossimi Passi

Hai ora tutti gli strumenti per creare Workers TypeScript robusti: routing con itty-router, gestione di variabili e secrets, sviluppo locale con wrangler dev e deploy in produzione. Il passo successivo è rendere i tuoi Workers stateful con i diversi layer di storage che Cloudflare mette a disposizione.

Prossimi Articoli della Serie

  • Articolo 3: Persistenza sull'Edge - KV, R2 e D1 - quando e come usare Workers KV per caching globale, R2 per object storage S3-compatibile senza egress fee, e D1 per query SQL relazionali.
  • Articolo 4: Durable Objects - la primitiva per stato fortemente consistente e WebSocket sull'edge.
  • Articolo 9: Testing con Miniflare e Vitest - unit test e integration test per Workers senza deploy.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti