INTEGRITY Dokumentace

Osvědčené postupy pro Workers

Osvědčené postupy pro Workers, které vycházejí z produkčních vzorů, interního používání v Cloudflare a běžných problémů pozorovaných napříč vývojářskou komunitou.

Konfigurace

Udržujte datum kompatibility aktuální

compatibility_date určuje, které funkce runtime a opravy chyb jsou pro váš Worker dostupné. Nastavením na dnešní datum u nových projektů zajistíte, že získáte nejnovější chování. Pravidelnou aktualizací u stávajících projektů získáte přístup k novým API a opravám, aniž byste museli měnit kód.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

Další informace najdete v tématu Data kompatibility.

Povolit nodejs_compat

nodejs_compat příznak kompatibility dává Workeru přístup k vestavěným modulům Node.js, jako je node:crypto, node:buffer, node:stream, a dalších. Na těchto modulech závisí mnoho knihoven, a zapnutí tohoto příznaku tak za běhu předchází nesrozumitelným chybám při importu.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

Další informace najdete v tématu Kompatibilita s Node.js.

Generování typů bindingů pomocí wrangler types

Nepište ručně svůj Env rozhraní. Spusťte wrangler types k vygenerování souboru s definicemi typů, který odpovídá vaší aktuální konfiguraci Wrangleru. Díky tomu se nesoulad mezi konfigurací a kódem odhalí už při kompilaci, a ne až při nasazení.

Znovu spusťte wrangler types kdykoli přidáte nebo přejmenujete binding.

npx wrangler types
src/index.js
// ✅ Good: Env is generated by wrangler types and always matches your config
// Do not manually define Env — it drifts from your actual bindings

export default {
	async fetch(request, env) {
		// env.MY_KV, env.MY_BUCKET, etc. are all correctly typed
		const value = await env.MY_KV.get("key");
		return new Response(value);
	},
};
src/index.ts
// ✅ Good: Env is generated by wrangler types and always matches your config
// Do not manually define Env — it drifts from your actual bindings

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// env.MY_KV, env.MY_BUCKET, etc. are all correctly typed
		const value = await env.MY_KV.get("key");
		return new Response(value);
	},
} satisfies ExportedHandler<Env>;

Další informace najdete v tématu wrangler types.

Ukládejte secrets pomocí wrangler secret, nikoli ve zdrojovém kódu

Secrets (klíče API, tokeny, přihlašovací údaje k databázi) se nikdy nesmí objevit ve vaší konfiguraci Wrangler ani ve zdrojovém kódu. Použijte wrangler secret put pro jejich bezpečné uložení a přistupujte k nim přes env za běhu. Pro lokální vývoj použijte .env soubor (a ujistěte se, že je ve vašem .gitignore). Další informace najdete v Proměnné prostředí.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	// ✅ Good: non-secret configuration lives in version control
	"vars": {
		"API_BASE_URL": "https://api.example.com",
	},

	// 🔴 Bad: never put secrets here
	// "API_KEY": "sk-live-abc123..."
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[vars]
API_BASE_URL = "https://api.example.com"

Chcete-li přidat secret, spusťte následující příkaz a po vyzvání zadejte secret interaktivně:

npx wrangler secret put API_KEY

Secrets můžete také přesměrovat (pipe) z jiných nástrojů nebo proměnných prostředí:

# Pipe from another CLI tool
npx some-cli-tool --get-secret | npx wrangler secret put API_KEY
# Pipe from an environment variable or .env file
echo "$API_KEY" | npx wrangler secret put API_KEY

Další informace najdete v tématu Tajné klíče.

Nakonfigurujte prostředí promyšleně

Prostředí Wrangleru vám umožňují nasadit stejný kód do samostatných Workerů pro produkci, staging a vývoj. Každé prostředí vytvoří samostatný Worker s názvem {name}-{env} (například my-api-production a my-api-staging).

Každé prostředí se zpracovává samostatně. Vazby a proměnné (vars) je nutné deklarovat pro každé prostředí zvlášť, protože se nedědí. Další informace najdete v nedědičné klíče. Kořenový Worker (bez přípony prostředí) představuje samostatný deployment. Pokud jej nechcete používat, nenasazujte bez uvedení prostředí pomocí --env.

{
	"name": "my-api",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	// This binding only applies to the root Worker
	"kv_namespaces": [{ "binding": "CACHE", "id": "dev-kv-id" }],

	"env": {
		// Production environment: deploys as "my-api-production"
		"production": {
			"kv_namespaces": [{ "binding": "CACHE", "id": "prod-kv-id" }],
			"routes": [
				{ "pattern": "api.example.com/*", "zone_name": "example.com" },
			],
		},
		// Staging environment: deploys as "my-api-staging"
		"staging": {
			"kv_namespaces": [{ "binding": "CACHE", "id": "staging-kv-id" }],
			"routes": [
				{ "pattern": "api-staging.example.com/*", "zone_name": "example.com" },
			],
		},
	},
}
name = "my-api"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[[kv_namespaces]]
binding = "CACHE"
id = "dev-kv-id"

[[env.production.kv_namespaces]]
binding = "CACHE"
id = "prod-kv-id"

[[env.production.routes]]
pattern = "api.example.com/*"
zone_name = "example.com"

[[env.staging.kv_namespaces]]
binding = "CACHE"
id = "staging-kv-id"

[[env.staging.routes]]
pattern = "api-staging.example.com/*"
zone_name = "example.com"

S tímto konfiguračním souborem nasadíte do staging takto:

npx wrangler deploy --env staging

Další informace najdete v tématu Prostředí.

Nastavit vlastní domény nebo trasy správně

Workers podporují dva mechanismy směrování, každý slouží jinému účelu:

Nejčastější chybou u routes bývá chybějící DNS záznam. Bez proxovaného DNS záznamu vrací requesty na daný hostname ERR_NAME_NOT_RESOLVED a nikdy se nedostanou k vašemu Workeru. Pokud nemáte skutečný origin server, přidejte proxovaný AAAA záznam směřující na 100:: jako zástupný symbol.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	// Option 1: Custom domain — Worker is the origin, DNS is managed automatically
	"routes": [{ "pattern": "api.example.com", "custom_domain": true }],

	// Option 2: Route — Worker runs in front of an existing origin
	// Requires a proxied DNS record for shop.example.com
	// "routes": [
	// 	{ "pattern": "shop.example.com/*", "zone_name": "example.com" }
	// ]
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[[routes]]
pattern = "api.example.com"
custom_domain = true

Další informace najdete v tématu Směrování.

Zpracování požadavků a odpovědí

Streamování těl požadavků a odpovědí

Bez ohledu na limity paměti je streamování velkých požadavků a odpovědí osvědčeným postupem v jakémkoli jazyce. Snižuje špičkové využití paměti a zlepšuje dobu do prvního bajtu. Workers mají limit paměti 128 MB, takže bufferování celého těla pomocí await response.text() nebo await request.arrayBuffer() způsobí u velkých payloadů pád vašeho Workeru.

U těl požadavků, která zpracováváte celá (JSON payloady, nahrávání souborů), vynucujte maximální velikost ještě před čtením. Tím zabráníte tomu, aby klienti odesílali data, která nechcete zpracovávat.

Streamujte data přes svůj Worker pomocí TransformStream pro přenos dat ze zdroje do cíle bez nutnosti držet vše v paměti.

src/index.js
// 🔴 Bad: buffers the entire response body in memory
const badHandler = {
	async fetch(request, env) {
		const response = await fetch("https://api.example.com/large-dataset");
		const text = await response.text();
		return new Response(text);
	},
};

// ✅ Good: stream the response body through without buffering
export default {
	async fetch(request, env) {
		const response = await fetch("https://api.example.com/large-dataset");
		return new Response(response.body, response);
	},
};
src/index.ts
// 🔴 Bad: buffers the entire response body in memory
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const response = await fetch("https://api.example.com/large-dataset");
		const text = await response.text();
		return new Response(text);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: stream the response body through without buffering
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const response = await fetch("https://api.example.com/large-dataset");
		return new Response(response.body, response);
	},
} satisfies ExportedHandler<Env>;

Když potřebujete zřetězit více odpovědí (například při načítání dat z několika upstream API), přesměrujte jednotlivá těla postupně do jediného zapisovatelného streamu. Předejdete tak ukládání kterékoli z odpovědí do vyrovnávací paměti.

src/concat.js
export default {
	async fetch(request, env) {
		const urls = [
			"https://api.example.com/part-1",
			"https://api.example.com/part-2",
			"https://api.example.com/part-3",
		];

		const { readable, writable } = new TransformStream();

		// ✅ Good: pipe each response body sequentially without buffering
		const pipeline = (async () => {
			for (const url of urls) {
				const response = await fetch(url);
				if (response.body) {
					// pipeTo with preventClose keeps the writable open for the next response
					await response.body.pipeTo(writable, {
						preventClose: true,
					});
				}
			}
			await writable.close();
		})();

		// Return the readable side immediately — data streams as it arrives
		return new Response(readable, {
			headers: { "Content-Type": "application/octet-stream" },
		});
	},
};
src/concat.ts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const urls = [
			"https://api.example.com/part-1",
			"https://api.example.com/part-2",
			"https://api.example.com/part-3",
		];

		const { readable, writable } = new TransformStream();

		// ✅ Good: pipe each response body sequentially without buffering
		const pipeline = (async () => {
			for (const url of urls) {
				const response = await fetch(url);
				if (response.body) {
					// pipeTo with preventClose keeps the writable open for the next response
					await response.body.pipeTo(writable, {
						preventClose: true,
					});
				}
			}
			await writable.close();
		})();

		// Return the readable side immediately — data streams as it arrives
		return new Response(readable, {
			headers: { "Content-Type": "application/octet-stream" },
		});
	},
} satisfies ExportedHandler<Env>;

Další informace najdete v tématu Streams.

Použijte waitUntil pro práci po odeslání odpovědi

ctx.waitUntil() vám umožňuje provádět úlohy po odeslání odpovědi klientovi, například analytiku, zápisy do cache, logování nebo webhook notifikace. Vaše odpověď tak zůstává rychlá a úlohy na pozadí se přesto dokončí.

Použijte ctx.waitUntil() pouze pro práci, která neovlivňuje odpověď. Pokud odpověď na dané práci závisí, await před vrácením odpovědi, nebo odpověď streamovat postupně, jak se práce dokončuje. Worker, který stále streamuje tělo odpovědi, zůstává aktivní bez ctx.waitUntil().

Existují dvě běžné nástrahy: destructuring ctx (což vede ke ztrátě this binding a vyvolá chybu „Illegal invocation“) a překročení 30sekundového waitUntil() časový limit po odeslání odpovědi nebo odpojení klienta.

src/index.js
// 🔴 Bad: destructuring ctx loses the `this` binding
const badHandler = {
	async fetch(request, env, ctx) {
		const { waitUntil } = ctx; // "Illegal invocation" at runtime
		waitUntil(fetch("https://analytics.example.com/events"));
		return new Response("OK");
	},
};

// ✅ Good: send the response immediately, do background work after
export default {
	async fetch(request, env, ctx) {
		const data = await processRequest(request);

		ctx.waitUntil(logToAnalytics(env, data));
		ctx.waitUntil(updateCache(env, data));

		return Response.json(data);
	},
};

async function logToAnalytics(env, data) {
	await fetch("https://analytics.example.com/events", {
		method: "POST",
		body: JSON.stringify(data),
	});
}

async function updateCache(env, data) {
	await env.CACHE.put("latest", JSON.stringify(data));
}
src/index.ts
// 🔴 Bad: destructuring ctx loses the `this` binding
const badHandler = {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const { waitUntil } = ctx; // "Illegal invocation" at runtime
		waitUntil(fetch("https://analytics.example.com/events"));
		return new Response("OK");
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: send the response immediately, do background work after
export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const data = await processRequest(request);

		ctx.waitUntil(logToAnalytics(env, data));
		ctx.waitUntil(updateCache(env, data));

		return Response.json(data);
	},
} satisfies ExportedHandler<Env>;

async function logToAnalytics(env: Env, data: unknown): Promise<void> {
	await fetch("https://analytics.example.com/events", {
		method: "POST",
		body: JSON.stringify(data),
	});
}

async function updateCache(env: Env, data: unknown): Promise<void> {
	await env.CACHE.put("latest", JSON.stringify(data));
}

Další informace najdete v tématu Kontext.

Architektura

Pro služby Cloudflare používejte bindings, nikoli REST API

Některé služby Cloudflare, jako R2, KV, D1, Queues a Workflows, jsou k dispozici jako vazby. Vazby jsou přímé odkazy v rámci procesu, které nevyžadují síťový přenos, ověřování ani dodatečnou latenci. Používání REST API přímo z Workeru je ztráta času a zbytečně to zvyšuje složitost.

src/index.js
// 🔴 Bad: calling the REST API from a Worker
const badHandler = {
	async fetch(request, env) {
		const response = await fetch(
			"https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/r2/buckets/BUCKET_NAME/objects/my-file",
			{ headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` } },
		);
		return new Response(response.body);
	},
};

// ✅ Good: use the binding directly — no network hop, no auth needed
export default {
	async fetch(request, env) {
		const object = await env.MY_BUCKET.get("my-file");

		if (!object) {
			return new Response("Not found", { status: 404 });
		}

		return new Response(object.body, {
			headers: {
				"Content-Type":
					object.httpMetadata?.contentType ?? "application/octet-stream",
			},
		});
	},
};
src/index.ts
// 🔴 Bad: calling the REST API from a Worker
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const response = await fetch(
			"https://api.cloudflare.com/client/v4/accounts/ACCOUNT_ID/r2/buckets/BUCKET_NAME/objects/my-file",
			{ headers: { Authorization: `Bearer ${env.CF_API_TOKEN}` } },
		);
		return new Response(response.body);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: use the binding directly — no network hop, no auth needed
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const object = await env.MY_BUCKET.get("my-file");

		if (!object) {
			return new Response("Not found", { status: 404 });
		}

		return new Response(object.body, {
			headers: {
				"Content-Type":
					object.httpMetadata?.contentType ?? "application/octet-stream",
			},
		});
	},
} satisfies ExportedHandler<Env>;

Použijte Queues a Workflows pro asynchronní práci a úlohy na pozadí

Dlouho běžící, opakovatelné nebo neurgentní úlohy by neměly blokovat požadavek. Použijte Queues a Workflows k přesunutí práce mimo kritickou cestu. Slouží k odlišným účelům:

Použijte Queues, když potřebujete oddělit producenta od konzumenta. Queues fungují jako message broker: jeden Worker odešle zprávu, druhý ji později zpracuje. Jsou správnou volbou pro fan-out (jedna událost spustí více konzumentů), buferování a dávkové zpracování (shromáždění zpráv před zápisem do navazující služby) a jednoduché jednokrokové úlohy na pozadí (odeslání e-mailu, spuštění webhooku, zápis do logu). Queues zajišťují doručení at-least-once s konfigurovatelnými opakovanými pokusy pro každou zprávu.

Použijte Workflows, když práce na pozadí sestává z více kroků, které na sobě navzájem závisí. Workflows jsou enginem pro trvalé provádění: návratová hodnota každého kroku se ukládá a pokud krok selže, znovu se spustí pouze tento krok, nikoli celá úloha. Jsou správnou volbou pro vícekrokové procesy (zúčtování platby kartou, poté vytvoření zásilky, poté odeslání potvrzení), pro dlouho běžící úlohy, které je potřeba pozastavit a později obnovit (čekání hodiny až dny na externí událost nebo lidské schválení přes step.waitForEvent()), a složitou podmíněnou logiku, kdy pozdější kroky závisí na výsledcích těch předchozích. Workflows mohou běžet hodiny, dny nebo týdny.

Použijte oboje společně když vstupní bod s vysokou propustností vede ke složitému zpracování. Queue může například bufferovat příchozí objednávky a consumer může pro každou objednávku vyžadující vícekrokové vyřízení vytvořit instanci Workflow.

src/index.js
export default {
	async fetch(request, env) {
		const order = await request.json();

		if (order.type === "simple") {
			// ✅ Queue: single-step background job — send a message for async processing
			await env.ORDER_QUEUE.send({
				orderId: order.id,
				action: "send-confirmation-email",
			});
		} else {
			// ✅ Workflow: multi-step durable process — payment, fulfillment, notification
			const instance = await env.FULFILLMENT_WORKFLOW.create({
				params: { orderId: order.id },
			});
		}

		return Response.json({ status: "accepted" }, { status: 202 });
	},
};
src/index.ts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const order = await request.json<{ id: string; type: string }>();

		if (order.type === "simple") {
			// ✅ Queue: single-step background job — send a message for async processing
			await env.ORDER_QUEUE.send({
				orderId: order.id,
				action: "send-confirmation-email",
			});
		} else {
			// ✅ Workflow: multi-step durable process — payment, fulfillment, notification
			const instance = await env.FULFILLMENT_WORKFLOW.create({
				params: { orderId: order.id },
			});
		}

		return Response.json({ status: "accepted" }, { status: 202 });
	},
} satisfies ExportedHandler<Env>;

Další informace najdete v tématu Queues a Workflows.

Pro komunikaci mezi Workery používejte service bindings

Když potřebuje jeden Worker zavolat jiný, použijte service bindings místo odesílání HTTP požadavku na veřejnou URL. Service bindings jsou bezplatné, obcházejí veřejný internet a podporují typově bezpečné RPC.

src/index.js
import { WorkerEntrypoint } from "cloudflare:workers";

// The "auth" Worker exposes RPC methods
export class AuthService extends WorkerEntrypoint {
	async verifyToken(token) {
		// Token verification logic
		return { userId: "user-123", valid: true };
	}
}

// The "api" Worker calls the auth Worker via a service binding
export default {
	async fetch(request, env) {
		const token = request.headers.get("Authorization")?.replace("Bearer ", "");

		if (!token) {
			return new Response("Unauthorized", { status: 401 });
		}

		// ✅ Good: call another Worker via service binding RPC — no network hop
		const auth = await env.AUTH_SERVICE.verifyToken(token);

		if (!auth.valid) {
			return new Response("Invalid token", { status: 403 });
		}

		return Response.json({ userId: auth.userId });
	},
};
src/index.ts
import { WorkerEntrypoint } from "cloudflare:workers";

// The "auth" Worker exposes RPC methods
export class AuthService extends WorkerEntrypoint {
	async verifyToken(
		token: string,
	): Promise<{ userId: string; valid: boolean }> {
		// Token verification logic
		return { userId: "user-123", valid: true };
	}
}

// The "api" Worker calls the auth Worker via a service binding
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const token = request.headers.get("Authorization")?.replace("Bearer ", "");

		if (!token) {
			return new Response("Unauthorized", { status: 401 });
		}

		// ✅ Good: call another Worker via service binding RPC — no network hop
		const auth = await env.AUTH_SERVICE.verifyToken(token);

		if (!auth.valid) {
			return new Response("Invalid token", { status: 403 });
		}

		return Response.json({ userId: auth.userId });
	},
} satisfies ExportedHandler<Env>;

Použijte Hyperdrive pro externí připojení k databázi

Vždy použijte Hyperdrive při připojování ke vzdálené databázi PostgreSQL nebo MySQL z Workeru. Hyperdrive udržuje regionální pool připojení v blízkosti vaší databáze, čímž eliminuje náklady na TCP handshake, vyjednávání TLS a navazování připojení pro každý požadavek. Kde je to možné, také ukládá výsledky dotazů do mezipaměti.

Vytvořte nový Client při každém požadavku. Hyperdrive spravuje podkladový pool, takže vytvoření klienta je rychlé. Vyžaduje nodejs_compat pro podporu databázových ovladačů.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<YOUR_HYPERDRIVE_ID>" }],
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[[hyperdrive]]
binding = "HYPERDRIVE"
id = "<YOUR_HYPERDRIVE_ID>"
src/index.js
import { Client } from "pg";

export default {
	async fetch(request, env) {
		// ✅ Good: create a new client per request — Hyperdrive pools the underlying connection
		const client = new Client({
			connectionString: env.HYPERDRIVE.connectionString,
		});

		try {
			await client.connect();
			const result = await client.query("SELECT id, name FROM users LIMIT 10");
			return Response.json(result.rows);
		} catch (e) {
			console.error(
				JSON.stringify({ message: "database query failed", error: String(e) }),
			);
			return Response.json({ error: "Database error" }, { status: 500 });
		}
	},
};

// 🔴 Bad: connecting directly to a remote database without Hyperdrive
// Every request pays the full TCP + TLS + auth cost (often 300-500ms)
const badHandler = {
	async fetch(request, env) {
		const client = new Client({
			connectionString: "postgres://user:[email protected]:5432/mydb",
		});
		await client.connect();
		const result = await client.query("SELECT id, name FROM users LIMIT 10");
		return Response.json(result.rows);
	},
};
src/index.ts
import { Client } from "pg";

export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// ✅ Good: create a new client per request — Hyperdrive pools the underlying connection
		const client = new Client({
			connectionString: env.HYPERDRIVE.connectionString,
		});

		try {
			await client.connect();
			const result = await client.query("SELECT id, name FROM users LIMIT 10");
			return Response.json(result.rows);
		} catch (e) {
			console.error(
				JSON.stringify({ message: "database query failed", error: String(e) }),
			);
			return Response.json({ error: "Database error" }, { status: 500 });
		}
	},
} satisfies ExportedHandler<Env>;

// 🔴 Bad: connecting directly to a remote database without Hyperdrive
// Every request pays the full TCP + TLS + auth cost (often 300-500ms)
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const client = new Client({
			connectionString: "postgres://user:[email protected]:5432/mydb",
		});
		await client.connect();
		const result = await client.query("SELECT id, name FROM users LIMIT 10");
		return Response.json(result.rows);
	},
} satisfies ExportedHandler<Env>;

Další informace najdete v tématu Hyperdrive.

Použijte Durable Objects pro WebSockets

Běžné Workers dokážou povýšit HTTP připojení na WebSockets, chybí jim ale trvalý stav a hibernace. Pokud je izolát odstraněn, spojení se ztratí, protože neexistuje trvalý aktér, který by ho udržel. Pro spolehlivá a dlouhotrvající WebSocket připojení použijte Durable Objects hodnotou Hibernation API. Durable Objects udržují připojení WebSocket otevřená, i když je objekt vyřazen z paměti, a po doručení zprávy se automaticky znovu probudí.

Použijte this.ctx.acceptWebSocket() místo ws.accept() abyste povolili hibernaci. Použijte setWebSocketAutoResponse pro ping/pong heartbeaty, které objekt neprobouzejí.

src/index.js
import { DurableObject } from "cloudflare:workers";

// Parent Worker: upgrades HTTP to WebSocket and routes to a Durable Object
export default {
	async fetch(request, env) {
		if (request.headers.get("Upgrade") !== "websocket") {
			return new Response("Expected WebSocket", { status: 426 });
		}

		const stub = env.CHAT_ROOM.getByName("default-room");
		return stub.fetch(request);
	},
};

// Durable Object: manages WebSocket connections with hibernation
export class ChatRoom extends DurableObject {
	constructor(ctx, env) {
		super(ctx, env);
		// Auto ping/pong without waking the object
		this.ctx.setWebSocketAutoResponse(
			new WebSocketRequestResponsePair("ping", "pong"),
		);
	}

	async fetch(request) {
		const pair = new WebSocketPair();
		const [client, server] = Object.values(pair);

		// ✅ Good: acceptWebSocket enables hibernation
		this.ctx.acceptWebSocket(server);

		return new Response(null, { status: 101, webSocket: client });
	}

	// Called when a message arrives — the object wakes from hibernation if needed
	async webSocketMessage(ws, message) {
		for (const conn of this.ctx.getWebSockets()) {
			conn.send(typeof message === "string" ? message : "binary");
		}
	}

	async webSocketClose(ws, code, reason, wasClean) {
		// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
		// auto-replies to Close frames. Calling close() is safe but no longer required.
		ws.close(code, reason);
	}
}
src/index.ts
import { DurableObject } from "cloudflare:workers";

// Parent Worker: upgrades HTTP to WebSocket and routes to a Durable Object
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		if (request.headers.get("Upgrade") !== "websocket") {
			return new Response("Expected WebSocket", { status: 426 });
		}

		const stub = env.CHAT_ROOM.getByName("default-room");
		return stub.fetch(request);
	},
} satisfies ExportedHandler<Env>;

// Durable Object: manages WebSocket connections with hibernation
export class ChatRoom extends DurableObject {
	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		// Auto ping/pong without waking the object
		this.ctx.setWebSocketAutoResponse(
			new WebSocketRequestResponsePair("ping", "pong"),
		);
	}

	async fetch(request: Request): Promise<Response> {
		const pair = new WebSocketPair();
		const [client, server] = Object.values(pair);

		// ✅ Good: acceptWebSocket enables hibernation
		this.ctx.acceptWebSocket(server);

		return new Response(null, { status: 101, webSocket: client });
	}

	// Called when a message arrives — the object wakes from hibernation if needed
	async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
		for (const conn of this.ctx.getWebSockets()) {
			conn.send(typeof message === "string" ? message : "binary");
		}
	}

	async webSocketClose(
		ws: WebSocket,
		code: number,
		reason: string,
		wasClean: boolean,
	) {
		// With web_socket_auto_reply_to_close (compat date >= 2026-04-07), the runtime
		// auto-replies to Close frames. Calling close() is safe but no longer required.
		ws.close(code, reason);
	}
}

Další informace najdete v tématu Osvědčené postupy pro WebSocket v Durable Objects.

Použijte Workers Static Assets pro nové projekty

Workers Static Assets je doporučený způsob nasazování statických webů, single-page aplikací a fullstack aplikací na Cloudflare. Pokud začínáte nový projekt, použijte místo Pages Workers. Pages nadále funguje, ale nové funkce a optimalizace se soustředí na Workers.

U čistě statického webu nasměrujte assets.directory ve výstupu buildu. Worker skript není potřeba. Pro full-stack aplikaci přidejte main vstupní bod a ASSETS binding k obsluze statických souborů vedle vašeho API.

{
	// Static site — no Worker script needed
	"name": "my-static-site",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	"assets": {
		"directory": "./dist",
	},
}
name = "my-static-site"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[assets]
directory = "./dist"

Další informace najdete v tématu Workers Static Assets.

Observabilita

Povolení Workers Logs a Traces

Produkční Workers bez observability jsou černá skříňka. Než nasadíte do produkce, povolte logy a trasování. Jakmile se objeví přerušovaná chyba, potřebujete k její diagnostice již sbírané data.

Povolte je v konfiguraci Wrangler a použijte head_sampling_rate pro řízení objemu a správu nákladů. Vzorkovací frekvence 1 zachytává vše; pro Workery s vysokým provozem tuto hodnotu snižte.

Použijte strukturované JSON logování s console.log aby protokoly byly prohledávatelné a filtrovatelné. Použijte console.error pro chyby a console.warn pro varování. Ty se zobrazí na správné úrovni závažnosti v dashboardu Workers Observability.

{
	"name": "my-worker",
	"main": "src/index.ts",
	// Set this to today's date
	"compatibility_date": "2026-08-28",
	"compatibility_flags": ["nodejs_compat"],

	"observability": {
		"enabled": true,
		"logs": {
			// Capture 100% of logs — lower this for high-traffic Workers
			"head_sampling_rate": 1,
		},
		"traces": {
			"enabled": true,
			"head_sampling_rate": 0.01, // Sample 1% of traces
		},
	},
}
name = "my-worker"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]

[observability]
enabled = true

  [observability.logs]
  head_sampling_rate = 1

  [observability.traces]
  enabled = true
  head_sampling_rate = 0.01
src/index.js
export default {
	async fetch(request, env) {
		const url = new URL(request.url);

		try {
			// ✅ Good: structured JSON — searchable and filterable in the dashboard
			console.log(
				JSON.stringify({
					message: "incoming request",
					method: request.method,
					path: url.pathname,
				}),
			);

			const result = await env.MY_KV.get(url.pathname);
			return new Response(result ?? "Not found", {
				status: result ? 200 : 404,
			});
		} catch (e) {
			// ✅ Good: console.error appears as "error" severity in Workers Observability
			console.error(
				JSON.stringify({
					message: "request failed",
					error: e instanceof Error ? e.message : String(e),
					path: url.pathname,
				}),
			);
			return Response.json({ error: "Internal server error" }, { status: 500 });
		}
	},
};

// 🔴 Bad: unstructured string logs are hard to query
const badHandler = {
	async fetch(request, env) {
		const url = new URL(request.url);
		console.log("Got a request to " + url.pathname);
		return new Response("OK");
	},
};
src/index.ts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);

		try {
			// ✅ Good: structured JSON — searchable and filterable in the dashboard
			console.log(
				JSON.stringify({
					message: "incoming request",
					method: request.method,
					path: url.pathname,
				}),
			);

			const result = await env.MY_KV.get(url.pathname);
			return new Response(result ?? "Not found", {
				status: result ? 200 : 404,
			});
		} catch (e) {
			// ✅ Good: console.error appears as "error" severity in Workers Observability
			console.error(
				JSON.stringify({
					message: "request failed",
					error: e instanceof Error ? e.message : String(e),
					path: url.pathname,
				}),
			);
			return Response.json({ error: "Internal server error" }, { status: 500 });
		}
	},
} satisfies ExportedHandler<Env>;

// 🔴 Bad: unstructured string logs are hard to query
const badHandler = {
	async fetch(request: Request, env: Env): Promise<Response> {
		const url = new URL(request.url);
		console.log("Got a request to " + url.pathname);
		return new Response("OK");
	},
} satisfies ExportedHandler<Env>;

Další informace najdete v tématu Workers Logs a Trasování.

Více informací o všech dostupných nástrojích pro observabilitu najdete v Workers Observability.

Vzory kódu

Neukládejte stav vázaný na požadavek do globálního rozsahu

Workers využívají izoláty opakovaně napříč požadavky. Proměnná nastavená během jednoho požadavku zůstává přítomná i v dalším. To způsobuje únik dat mezi požadavky, zastaralý stav a chyby "Cannot perform I/O on behalf of a different request".

Předejte stav pomocí argumentů funkce nebo jej uložte do env bindingy. Nikdy v proměnných na úrovni modulu.

src/index.js
// 🔴 Bad: global mutable state leaks between requests
let currentUser = null;

const badHandler = {
	async fetch(request, env, ctx) {
		// Storing request-scoped data globally means the next request sees stale data
		currentUser = request.headers.get("X-User-Id");
		const result = await handleRequest(currentUser, env);
		return Response.json(result);
	},
};

// ✅ Good: pass request-scoped data through function arguments
export default {
	async fetch(request, env, ctx) {
		const userId = request.headers.get("X-User-Id");
		const result = await handleRequest(userId, env);

		return Response.json(result);
	},
};

async function handleRequest(userId, env) {
	return { userId };
}
src/index.ts
// 🔴 Bad: global mutable state leaks between requests
let currentUser: string | null = null;

const badHandler = {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		// Storing request-scoped data globally means the next request sees stale data
		currentUser = request.headers.get("X-User-Id");
		const result = await handleRequest(currentUser, env);
		return Response.json(result);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: pass request-scoped data through function arguments
export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const userId = request.headers.get("X-User-Id");
		const result = await handleRequest(userId, env);

		return Response.json(result);
	},
} satisfies ExportedHandler<Env>;

async function handleRequest(userId: string | null, env: Env): Promise<object> {
	return { userId };
}

Další informace najdete v tématu Chyby Workers.

Vždy použijte await nebo waitUntil pro své promises

A Promise která není await, return, nebo nebyl předán do ctx.waitUntil() je floating promise. Floating promises způsobují tiché chyby: zahozené výsledky, potlačené chyby a nedokončenou práci. Běhové prostředí Workers může ukončit váš izolát dříve, než se floating promise dokončí.

Zvolte podle toho, zda odpověď závisí na dané úloze. Použijte await nebo return pro práci, která musí být dokončena, než je odpověď správná. Použijte ctx.waitUntil() pro práci, která se spouští po odeslání odpovědi a stihne se dokončit během waitUntil() časový limit.

Povolte no-floating-promises lintovací pravidlo, které tyto případy zachytí již při vývoji. Pokud používáte ESLint, zapněte @typescript-eslint/no-floating-promises. Pokud používáte oxlint, povolte typescript/no-floating-promises.

# ESLint (typescript-eslint)
npx eslint --rule '{"@typescript-eslint/no-floating-promises": "error"}' src/

# oxlint
npx oxlint --deny typescript/no-floating-promises src/
src/index.js
export default {
	async fetch(request, env, ctx) {
		const data = await request.json();

		// 🔴 Bad: floating promise — result is dropped, errors are swallowed
		fetch("https://api.example.com/webhook", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: await if you need the result before responding
		const response = await fetch("https://api.example.com/process", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: waitUntil if you do not need the result before responding
		ctx.waitUntil(
			fetch("https://api.example.com/webhook", {
				method: "POST",
				body: JSON.stringify(data),
			}),
		);

		return new Response("OK");
	},
};
src/index.ts
export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		const data = await request.json();

		// 🔴 Bad: floating promise — result is dropped, errors are swallowed
		fetch("https://api.example.com/webhook", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: await if you need the result before responding
		const response = await fetch("https://api.example.com/process", {
			method: "POST",
			body: JSON.stringify(data),
		});

		// ✅ Good: waitUntil if you do not need the result before responding
		ctx.waitUntil(
			fetch("https://api.example.com/webhook", {
				method: "POST",
				body: JSON.stringify(data),
			}),
		);

		return new Response("OK");
	},
} satisfies ExportedHandler<Env>;

Security

Použijte Web Crypto pro bezpečné generování tokenů

Workers runtime poskytuje Web Crypto API pro kryptografické operace. Použijte crypto.randomUUID() pro jedinečné identifikátory a crypto.getRandomValues() pro náhodné bajty. Nikdy nepoužívejte Math.random() pro cokoli citlivého z hlediska zabezpečení. Není kryptograficky bezpečný.

Node.js node:crypto je také plně podporována, pokud nodejs_compat je povoleno, takže můžete použít to rozhraní API, které preferujete vy nebo vaše knihovny.

src/index.js
export default {
	async fetch(request, env) {
		// 🔴 Bad: Math.random() is predictable and not suitable for security
		const badToken = Math.random().toString(36).substring(2);

		// ✅ Good: cryptographically secure random UUID
		const sessionId = crypto.randomUUID();

		// ✅ Good: cryptographically secure random bytes for tokens
		const tokenBytes = new Uint8Array(32);
		crypto.getRandomValues(tokenBytes);
		const token = Array.from(tokenBytes)
			.map((b) => b.toString(16).padStart(2, "0"))
			.join("");

		return Response.json({ sessionId, token });
	},
};
src/index.ts
export default {
	async fetch(request: Request, env: Env): Promise<Response> {
		// 🔴 Bad: Math.random() is predictable and not suitable for security
		const badToken = Math.random().toString(36).substring(2);

		// ✅ Good: cryptographically secure random UUID
		const sessionId = crypto.randomUUID();

		// ✅ Good: cryptographically secure random bytes for tokens
		const tokenBytes = new Uint8Array(32);
		crypto.getRandomValues(tokenBytes);
		const token = Array.from(tokenBytes)
			.map((b) => b.toString(16).padStart(2, "0"))
			.join("");

		return Response.json({ sessionId, token });
	},
} satisfies ExportedHandler<Env>;

Při porovnávání tajných hodnot (API klíčů, tokenů, HMAC podpisů) použijte crypto.subtle.timingSafeEqual() aby se zabránilo časovým útokům postranním kanálem. Neukončujte porovnávání předčasně při neshodě délky. Nejprve zakódujte obě hodnoty do hashe s pevnou velikostí.

src/verify.js
async function verifyToken(provided, expected) {
	const encoder = new TextEncoder();

	// ✅ Good: hash both values to a fixed size, then compare in constant time
	// This avoids leaking the length of the expected value
	const [providedHash, expectedHash] = await Promise.all([
		crypto.subtle.digest("SHA-256", encoder.encode(provided)),
		crypto.subtle.digest("SHA-256", encoder.encode(expected)),
	]);

	return crypto.subtle.timingSafeEqual(providedHash, expectedHash);
}

// 🔴 Bad: direct string comparison leaks timing information
function verifyTokenInsecure(provided, expected) {
	return provided === expected;
}
src/verify.ts
async function verifyToken(
	provided: string,
	expected: string,
): Promise<boolean> {
	const encoder = new TextEncoder();

	// ✅ Good: hash both values to a fixed size, then compare in constant time
	// This avoids leaking the length of the expected value
	const [providedHash, expectedHash] = await Promise.all([
		crypto.subtle.digest("SHA-256", encoder.encode(provided)),
		crypto.subtle.digest("SHA-256", encoder.encode(expected)),
	]);

	return crypto.subtle.timingSafeEqual(providedHash, expectedHash);
}

// 🔴 Bad: direct string comparison leaks timing information
function verifyTokenInsecure(provided: string, expected: string): boolean {
	return provided === expected;
}

Nepoužívejte passThroughOnException jako zpracování chyb

passThroughOnException() je mechanismus fail-open, který odesílá požadavky na váš origin server, pokud váš Worker vyvolá neošetřenou výjimku. Může být užitečný při migraci z origin serveru, ale skrývá chyby a ztěžuje ladění. Používejte explicitní try...catch bloky nahraďte strukturovanými chybovými odpověďmi.

src/index.js
// 🔴 Bad: hides errors by falling through to origin
const badHandler = {
	async fetch(request, env, ctx) {
		ctx.passThroughOnException();
		const result = await handleRequest(request, env);
		return Response.json(result);
	},
};

// ✅ Good: explicit error handling with structured responses
export default {
	async fetch(request, env, ctx) {
		try {
			const result = await handleRequest(request, env);
			return Response.json(result);
		} catch (error) {
			const message = error instanceof Error ? error.message : "Unknown error";

			console.error(
				JSON.stringify({
					message: "unhandled error",
					error: message,
					path: new URL(request.url).pathname,
				}),
			);

			return Response.json({ error: "Internal server error" }, { status: 500 });
		}
	},
};

async function handleRequest(request, env) {
	return { status: "ok" };
}
src/index.ts
// 🔴 Bad: hides errors by falling through to origin
const badHandler = {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		ctx.passThroughOnException();
		const result = await handleRequest(request, env);
		return Response.json(result);
	},
} satisfies ExportedHandler<Env>;

// ✅ Good: explicit error handling with structured responses
export default {
	async fetch(
		request: Request,
		env: Env,
		ctx: ExecutionContext,
	): Promise<Response> {
		try {
			const result = await handleRequest(request, env);
			return Response.json(result);
		} catch (error) {
			const message = error instanceof Error ? error.message : "Unknown error";

			console.error(
				JSON.stringify({
					message: "unhandled error",
					error: message,
					path: new URL(request.url).pathname,
				}),
			);

			return Response.json({ error: "Internal server error" }, { status: 500 });
		}
	},
} satisfies ExportedHandler<Env>;

async function handleRequest(request: Request, env: Env): Promise<object> {
	return { status: "ok" };
}

Vývoj a testování

Testujte pomocí @cloudflare/vitest-plugin

@cloudflare/vitest-plugin balíček spouští vaše testy uvnitř runtime Workers, což vám během testování dává přístup ke skutečným bindingům (KV, R2, D1, Durable Objects). Díky tomu odhalíte problémy, které testy založené na Node.js přehlédnou, například nepodporovaná API nebo chybějící kompatibilní příznaky.

Jedno známé úskalí: plugin Vitest automaticky vkládá nodejs_compat, takže testy projdou, i když vaše konfigurace Wrangler tento příznak nemá. Vždy si ověřte své wrangler.jsonc zahrnuje nodejs_compat pokud váš kód závisí na vestavěných modulech Node.js.

test/index.test.js
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:workers";

describe("KV operations", () => {
	it("should store and retrieve a value", async () => {
		await env.MY_KV.put("key", "value");
		const result = await env.MY_KV.get("key");
		expect(result).toBe("value");
	});

	it("should return null for missing keys", async () => {
		const result = await env.MY_KV.get("nonexistent");
		// ✅ Good: test the null case explicitly
		expect(result).toBeNull();
	});
});
test/index.test.ts
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:workers";

describe("KV operations", () => {
	it("should store and retrieve a value", async () => {
		await env.MY_KV.put("key", "value");
		const result = await env.MY_KV.get("key");
		expect(result).toBe("value");
	});

	it("should return null for missing keys", async () => {
		const result = await env.MY_KV.get("nonexistent");
		// ✅ Good: test the null case explicitly
		expect(result).toBeNull();
	});
});

Další informace najdete v tématu Testování s Vitest.