ゼロから生産現場の労働者へ

前の記事では、V8 アイソレートとそれが風邪を排除する理由を理解しました。 が始まります。この記事では、具体的なもの、つまり管理する労働者を構築します。 ルーティング、環境変数の読み取り、シークレットの使用、JSON での応答。最後に Cloudflareアカウントにワーカーがデプロイされ、世界中からアクセス可能になります 起動から 1ms 未満で。

何を学ぶか

  • Wrangler CLI と TypeScript を使用した開発環境のセットアップ
  • ファイル構造 wrangler.toml そしてすべての主要なフィールド
  • フェッチ ハンドラー: request、env、ExecutionContext の詳細
  • 複雑な URL パターンに対する手動および itty ルーター ルーティング
  • 環境変数、シークレット、およびタイプセーフな TypeScript でそれらにアクセスする方法
  • ローカルテスト wrangler dev そしてホットリロード
  • 本番環境および環境管理 (ステージング/本番) へのデプロイメント
  • カスタムドメインとルートパターン

前提条件と初期セットアップ

無料のCloudflareアカウントとNode.js 18+が必要です。無料利用枠 1 日あたり 100,000 件のリクエストが含まれており、開発や 個人的なプロジェクト。

# 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

ワーカープロジェクトを作成する

Wrangler がコマンドを提供します create 誰とプロジェクトを棚上げするか TypeScript と必要なすべての依存関係:

# 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: 中央構成

ファイル wrangler.toml これは Worker 構成の中心です。 各フィールドは展開動作に影響を与えます。

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

フェッチ ハンドラー: 完全な構造

各ワーカーはメソッドを使用してデフォルトのオブジェクトをエクスポートする必要があります fetch。 各パラメータを詳しく見てみましょう。

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

itty-router による高度なルーティング

多くのルートを持つアプリケーションの場合、手動ルーティングの記述は冗長になります。図書館 ittyルーター 労働者向けに特別に設計されています。 小文字 (1KB 未満) で、同じ標準 Web 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,
};

環境変数とシークレットの管理

ワーカーは 2 種類の構成値を区別します。 変数 (コードと Wrangler で表示されますが、暗号化されません) e 秘密 (暗号化され、実行時にワーカー内でのみ表示され、ダッシュボードでは決して消去されません)。

# 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

変数とシークレット: いつ何を使用するか

  • 変数 ([vars]): パブリック API URL、機能フラグ、構成 環境の。これらは wrangler.toml で表示されるため、機密データには使用しないでください。 そしてCloudflareダッシュボードで。
  • 秘密: API キー、データベース パスワード、JWT トークン、その他のデータ 敏感な。これらは暗号化されており、ログであっても平文で公開されることはありません。

Wrangler dev によるローカル開発

wrangler dev Workers ランタイムをシミュレートするローカル サーバーを起動します。 自動ホットリロード:

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

ローカル シークレットの場合は、ファイルを作成します .dev.vars (追加予定 .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 は、依存関係をバンドルして TypeScript をコンパイルする単一のコマンドです。 esbuild を使用して結果を 300 以上の Cloudflare PoP にアップロードします。

# 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

カスタムドメインとルートパターン

ワーカーをカスタム ドメイン (Cloudflare 上にある必要があります) に関連付けるには、次のように設定します。 のルート 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)

エラー管理および応答ヘルパー

堅牢なエラー処理パターンでは、グローバル ラッパーを使用します。

// 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: ワーカーにおけるクロスオリジン管理

異なるドメインのブラウザから呼び出す必要がある API については、CORS を構成します ワーカー内で直接:

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

結論と次のステップ

これで、堅牢な TypeScript ワーカーを作成するためのツールがすべて揃いました。 itty-router、変数とシークレットの管理、ローカル開発 wrangler dev そして本番環境にデプロイします。次のステップは、ワーカーをステートフルにすることです Cloudflareが利用できるさまざまなストレージレイヤーを使用します。

シリーズの次の記事

  • 第3条: エッジ永続性 — KV、R2、および D1 — Workers KV をグローバル キャッシュに、R2 をオブジェクト ストレージに使用する場合とその方法 S3 と互換性があり、下り料金はかかりません。D1 はリレーショナル SQL クエリに対応します。
  • 第4条: 永続オブジェクト — 状態プリミティブ 強力な一貫性とエッジでの WebSocket。
  • 第9条: Miniflare と Vitest を使用したテスト — 単体テスト デプロイメントを行わないワーカーの統合テストも可能です。