Cloudflare Workers let you run JavaScript (or WebAssembly) at the edge — meaning your code runs in one of Cloudflare's 300+ data centres, as close to the user as possible. Cold start times are under 5ms. This isn't a container spinning up; it's a V8 isolate.
Prerequisites
- A Cloudflare account (free tier works)
- Node.js 18+
wranglerCLI
npm install -g wrangler
wrangler login
Your First Worker
wrangler init my-worker
cd my-worker
The generated src/index.ts (or .js) exports a fetch handler:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/hello') {
return new Response('Hello, World!', {
headers: { 'Content-Type': 'text/plain' },
});
}
return new Response('Not Found', { status: 404 });
},
};
Run locally:
wrangler dev
Deploy:
wrangler deploy
Routing
Workers don't have a built-in router. For simple cases, branch on pathname:
const url = new URL(request.url);
switch (url.pathname) {
case '/': return handleHome(request, env);
case '/api': return handleApi(request, env);
default: return new Response('Not Found', { status: 404 });
}
For anything more complex, use itty-router — it's tiny and designed for Workers:
import { Router } from 'itty-router';
const router = Router();
router
.get('/posts', ({ env }) => getPosts(env))
.get('/posts/:id', ({ params, env }) => getPost(params.id, env))
.all('*', () => new Response('Not Found', { status: 404 }));
export default { fetch: router.fetch };
Environment Variables & Secrets
Declare bindings in wrangler.toml:
[vars]
ENVIRONMENT = "production"
API_URL = "https://api.example.com"
For secrets (API keys, tokens), never put them in wrangler.toml. Use:
wrangler secret put MY_API_KEY
Access them in your Worker via env:
async fetch(request: Request, env: Env) {
const key = env.MY_API_KEY;
// ...
}
KV Storage
Workers KV is a key-value store replicated globally. Good for: caches, config, session state.
Create a namespace:
wrangler kv:namespace create "CACHE"
Add the binding to wrangler.toml:
[[kv_namespaces]]
binding = "CACHE"
id = "your-namespace-id-here"
Use it:
// Read
const value = await env.CACHE.get('my-key');
// Write (with optional TTL)
await env.CACHE.put('my-key', JSON.stringify(data), {
expirationTtl: 3600, // seconds
});
// Delete
await env.CACHE.delete('my-key');
Caching API Responses
Use the Cache API to avoid hitting your origin on every request:
async function cachedFetch(url: string, ttl = 60): Promise<Response> {
const cache = caches.default;
const cacheKey = new Request(url);
const cached = await cache.match(cacheKey);
if (cached) return cached;
const res = await fetch(url);
const clone = res.clone();
// Store with cache-control
const headers = new Headers(res.headers);
headers.set('Cache-Control', `public, max-age=${ttl}`);
await cache.put(cacheKey, new Response(clone.body, { headers }));
return res;
}
Useful Patterns
CORS headers
const CORS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
};
export default {
async fetch(request: Request): Promise<Response> {
if (request.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: CORS });
}
const res = await handleRequest(request);
return new Response(res.body, {
status: res.status,
headers: { ...Object.fromEntries(res.headers), ...CORS },
});
},
};
JSON helper
function json(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { 'Content-Type': 'application/json' },
});
}
Workers are genuinely one of the most pleasant platforms to build on. The local dev experience with wrangler dev is fast, the deploy is a single command, and the free tier is generous enough for most personal projects.