Cloudflare Workers: Build and Deploy Serverless Apps at the Edge

Cloudflare Workers: Build and Deploy Serverless Apps at the Edge


Cloudflare Workers let you run JavaScript, TypeScript, Rust, or Python on Cloudflare’s global network of 300+ data centers. Your code runs within milliseconds of your users — no servers to manage, no cold starts, and a generous free tier of 100,000 requests per day.

Whether you need an API, a URL shortener, an authentication proxy, or a full web application, Workers can handle it. This guide covers everything from your first Worker to production deployment with KV storage, environment variables, and custom domains.

What Are Cloudflare Workers?

Workers are serverless functions that run on Cloudflare’s edge network. Unlike traditional serverless (AWS Lambda, Google Cloud Functions) which runs in a few regions, Workers execute in every Cloudflare data center worldwide — meaning sub-50ms response times for users everywhere.

Key features:

  • Zero cold starts: Workers use V8 isolates (not containers), so there’s no startup delay
  • Global by default: Code runs in 300+ cities automatically
  • Free tier: 100,000 requests/day, 10ms CPU time per request
  • Paid plan: $5/month for 10 million requests, 50ms CPU time
  • Multiple languages: JavaScript, TypeScript, Rust, Python, WASM
  • Integrated storage: KV, R2 (object storage), D1 (SQLite), Durable Objects

Getting Started

Install Wrangler CLI

Wrangler is Cloudflare’s official CLI for Workers development:

npm install -g wrangler

# Or using npx (no global install needed)
npx wrangler

Authenticate

wrangler login

This opens a browser window to authenticate with your Cloudflare account.

Create a New Project

# Create a new Worker project
wrangler init my-worker

# Or with a template
wrangler init my-api --template https://github.com/cloudflare/workers-sdk/tree/main/templates/worker-router

Choose your options:

  • TypeScript or JavaScript
  • Whether to use a wrangler.toml config file (yes)
  • Whether to create a Git repo

Project Structure

my-worker/
├── src/
│   └── index.ts        # Your Worker code
├── wrangler.toml       # Configuration
├── package.json
└── tsconfig.json

Writing Your First Worker

Basic Worker (ES Module Syntax)

// src/index.ts
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    return new Response("Hello from Cloudflare Workers!", {
      headers: { "content-type": "text/plain" },
    });
  },
};

Handle Different Routes

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    switch (url.pathname) {
      case "/":
        return new Response("Home page");
      
      case "/api/hello":
        return Response.json({ message: "Hello, World!" });
      
      case "/api/time":
        return Response.json({ time: new Date().toISOString() });
      
      default:
        return new Response("Not Found", { status: 404 });
    }
  },
};

Handle HTTP Methods

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/api/data") {
      switch (request.method) {
        case "GET":
          return Response.json({ items: ["a", "b", "c"] });
        
        case "POST":
          const body = await request.json();
          return Response.json({ received: body }, { status: 201 });
        
        case "DELETE":
          return new Response(null, { status: 204 });
        
        default:
          return new Response("Method Not Allowed", { status: 405 });
      }
    }

    return new Response("Not Found", { status: 404 });
  },
};

Local Development

Run Locally with Wrangler Dev

wrangler dev

This starts a local development server (default: http://localhost:8787) that simulates the Workers runtime. Changes are hot-reloaded automatically.

# Specify a port
wrangler dev --port 3000

# Use remote mode (runs on Cloudflare's edge for testing bindings)
wrangler dev --remote

Test with curl

curl http://localhost:8787/
curl http://localhost:8787/api/hello
curl -X POST http://localhost:8787/api/data -d '{"name":"test"}' -H "Content-Type: application/json"

Configuration (wrangler.toml)

name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"

# Custom domain routing
routes = [
  { pattern = "api.yourdomain.com/*", zone_name = "yourdomain.com" }
]

# Or use a workers.dev subdomain
# workers_dev = true

# Environment variables
[vars]
API_KEY = "public-value"
ENVIRONMENT = "production"

# KV Namespace binding
[[kv_namespaces]]
binding = "MY_KV"
id = "your-kv-namespace-id"

# R2 bucket binding
[[r2_buckets]]
binding = "MY_BUCKET"
bucket_name = "my-bucket"

# D1 database binding
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"

Deploying Workers

Deploy to Production

wrangler deploy

Your Worker is now live at my-worker.<your-subdomain>.workers.dev.

Deploy to a Custom Domain

  1. Add a route in wrangler.toml:
routes = [
  { pattern = "api.yourdomain.com/*", zone_name = "yourdomain.com" }
]
  1. Deploy:
wrangler deploy

Cloudflare automatically sets up the DNS and SSL.

Deploy with a Custom Workers.dev Subdomain

name = "my-api"
workers_dev = true

This deploys to my-api.your-account.workers.dev.

Workers KV (Key-Value Storage)

KV is a global, low-latency key-value store. Perfect for configuration, feature flags, URL shorteners, and caching.

Create a KV Namespace

# Create for production
wrangler kv namespace create "MY_KV"

# Create for preview/dev
wrangler kv namespace create "MY_KV" --preview

Add the returned IDs to wrangler.toml:

[[kv_namespaces]]
binding = "MY_KV"
id = "abc123"
preview_id = "def456"

Use KV in Your Worker

export interface Env {
  MY_KV: KVNamespace;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (request.method === "GET") {
      // Read from KV
      const value = await env.MY_KV.get(url.pathname);
      if (value) {
        return new Response(value);
      }
      return new Response("Not found", { status: 404 });
    }

    if (request.method === "PUT") {
      // Write to KV
      const body = await request.text();
      await env.MY_KV.put(url.pathname, body, {
        expirationTtl: 86400, // Expire after 24 hours
      });
      return new Response("Stored", { status: 201 });
    }

    return new Response("Method not allowed", { status: 405 });
  },
};

Manage KV from the CLI

# Write a value
wrangler kv key put --binding MY_KV "mykey" "myvalue"

# Read a value
wrangler kv key get --binding MY_KV "mykey"

# List keys
wrangler kv key list --binding MY_KV

# Delete a key
wrangler kv key delete --binding MY_KV "mykey"

# Bulk upload from JSON
wrangler kv bulk put --binding MY_KV data.json

Environment Variables and Secrets

Public Variables (wrangler.toml)

[vars]
PUBLIC_API_URL = "https://api.example.com"
ENVIRONMENT = "production"

Secrets (Encrypted)

# Set a secret (not stored in wrangler.toml)
wrangler secret put API_SECRET
# Enter value when prompted

# List secrets
wrangler secret list

# Delete a secret
wrangler secret delete API_SECRET

Access in your Worker:

export interface Env {
  PUBLIC_API_URL: string;
  API_SECRET: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // env.PUBLIC_API_URL = "https://api.example.com"
    // env.API_SECRET = your encrypted secret
    const response = await fetch(env.PUBLIC_API_URL, {
      headers: { Authorization: `Bearer ${env.API_SECRET}` },
    });
    return response;
  },
};

Practical Examples

URL Shortener

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const slug = url.pathname.slice(1);

    if (!slug) {
      return new Response("URL Shortener - provide a slug", { status: 200 });
    }

    const destination = await env.URLS.get(slug);
    if (destination) {
      return Response.redirect(destination, 301);
    }

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

CORS Proxy

export default {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
    const targetUrl = url.searchParams.get("url");

    if (!targetUrl) {
      return new Response("Provide ?url= parameter", { status: 400 });
    }

    const response = await fetch(targetUrl);
    const newResponse = new Response(response.body, response);
    newResponse.headers.set("Access-Control-Allow-Origin", "*");
    return newResponse;
  },
};

JSON API with D1 (SQLite)

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/api/users" && request.method === "GET") {
      const { results } = await env.DB.prepare("SELECT * FROM users").all();
      return Response.json(results);
    }

    if (url.pathname === "/api/users" && request.method === "POST") {
      const { name, email } = await request.json();
      await env.DB.prepare("INSERT INTO users (name, email) VALUES (?, ?)")
        .bind(name, email)
        .run();
      return Response.json({ success: true }, { status: 201 });
    }

    return new Response("Not Found", { status: 404 });
  },
};

Wrangler CLI Reference

CommandPurpose
wrangler initCreate a new Worker project
wrangler devStart local development server
wrangler deployDeploy to Cloudflare
wrangler tailStream live logs from production
wrangler secret put NAMESet an encrypted secret
wrangler kv namespace createCreate a KV namespace
wrangler d1 createCreate a D1 database
wrangler r2 bucket createCreate an R2 bucket
wrangler deleteDelete a deployed Worker
wrangler whoamiShow current account info

Summary

Cloudflare Workers provide a fast, affordable way to run serverless code globally. With zero cold starts, a generous free tier, and integrated storage options (KV, R2, D1), they’re ideal for APIs, edge processing, URL shorteners, and full-stack applications.

Key resources:

Start with wrangler init, write a fetch handler, and wrangler deploy. Your code is running globally in seconds.