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+
  • wrangler CLI
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.