Primul tău lucrător Cloudflare: Preluează Handler, Wrangler și Deploy
Un ghid pas cu pas pentru crearea, testarea și implementarea unui Cloudflare Worker din zero: de la handlerul de preluare la implementarea wrangler, trecând prin variabilele de mediu, secrete și rutare avansată.
De la zero la muncitor în producție
În articolul precedent am înțeles izolatele V8 și de ce elimină frigul începe. În acest articol construim ceva concret: un Muncitor care se descurcă rutare, citește variabilele de mediu, folosește secrete și răspunde cu JSON. La sfârşit veți avea un lucrător implementat în contul dvs. Cloudflare, accesibil la nivel global în mai puțin de 1 ms de pornire.
Ce vei învăța
- Configurarea mediului de dezvoltare cu Wrangler CLI și TypeScript
- Structura fișierului
wrangler.tomlși toate domeniile principale - Managerul de preluare: request, env, ExecutionContext în detaliu
- Rutare manuală și itty-router pentru modele complexe de adrese URL
- Variabilele de mediu, secretele și cum să le accesați în TypeScript sigur de tip
- Test local cu
wrangler devși reîncărcare la cald - Implementare în producție și managementul mediului (staging/prod)
- Domenii personalizate și modele de rută
Cerințe preliminare și configurare inițială
Veți avea nevoie de un cont Cloudflare gratuit și de Node.js 18+. Nivelul gratuit include 100.000 de solicitări pe zi, mai mult decât suficient pentru dezvoltare și proiecte personale.
# 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
Creați proiectul lucrător
Wrangler oferă o comandă create cu care raftează proiectul
TypeScript și toate dependențele necesare:
# 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: Configurația centrală
Dosarul wrangler.toml este inima configurației Worker.
Fiecare câmp are implicații asupra comportamentului de implementare:
# 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: Anatomie completă
Fiecare lucrător trebuie să exporte un obiect implicit cu o metodă fetch.
Să vedem fiecare parametru în detaliu:
// 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 });
}
Rutare avansată cu itty-router
Pentru aplicațiile cu multe rute, scrierea de rutare manuală devine verbosă. Biblioteca itty-router este conceput special pentru Muncitori: este litere mici (mai puțin de 1 KB) și utilizează aceleași API-uri 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,
};
Gestionarea variabilelor și secretelor de mediu
Lucrătorii disting două tipuri de valori de configurare: variabile (vizibil în cod și în Wrangler, necriptat) e secrete (criptat, vizibil numai în Worker în timpul execuției, niciodată clar în tabloul de bord).
# 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
Variabile vs Secrete: Când să folosiți Ce
- Variabile ([vars]): Adrese URL publice API, semnalizatoare de caracteristici, configurație a mediului. Nu le utilizați pentru date sensibile, deoarece sunt vizibile în wrangler.toml și în tabloul de bord Cloudflare.
- Secrete: Chei API, parole baze de date, jetoane JWT, orice date sensibile. Sunt criptate și nu sunt niciodată expuse în clar, nici măcar în jurnale.
Dezvoltare locală cu wrangler dev
wrangler dev pornește un server local care simulează runtime-ul Workers cu
reîncărcare automată la cald:
# 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",...}
Pentru secretele locale, creați un fișier .dev.vars (de adăugat la
.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
Implementați în producție
Deploy este o singură comandă care compilează TypeScript-ul, grupând dependențele cu esbuild și încărcați rezultatul în cele peste 300 de puncte de lucru 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
Domenii personalizate și modele de rută
Pentru a asocia Lucrătorul cu un domeniu personalizat (care trebuie să fie pe Cloudflare), configurați
traseele în 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)
Ajutor pentru gestionarea erorilor și răspuns
Un model robust de gestionare a erorilor folosește un wrapper global:
// 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: Managementul Cross-Original în Muncitori
Pentru API-urile care trebuie apelate din browsere de pe diferite domenii, configurați CORS direct în Lucrător:
// 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;
},
};
Concluzii și pașii următori
Acum aveți toate instrumentele pentru a crea lucrători TypeScript robusti: rutare cu
itty-router, managementul variabilelor și secretelor, dezvoltarea locală cu wrangler dev
și implementați în producție. Următorul pas este să vă faceți lucrătorii cu stare
cu diferitele straturi de stocare pe care Cloudflare le pune la dispoziție.
Următoarele articole din serie
- Articolul 3: Persistența marginii - KV, R2 și D1 - când și cum să utilizați Workers KV pentru stocarea în cache globală, R2 pentru stocarea obiectelor Compatibil cu S3, fără taxă de ieșire și D1 pentru interogări SQL relaționale.
- Articolul 4: Obiecte durabile — starea primitivă puternic consistent și WebSocket la margine.
- Articolul 9: Testare cu Miniflare și Vitest — teste unitare și teste de integrare pentru lucrătorii fără implementare.







