workers

v2026.09.25

Core Workers fundamentals including handlers, configuration, and Service Bindings. Load when creating new Workers, configuring wrangler.jsonc, implementing fetch/scheduled/queue handlers, using Service Bindings for RPC, generating types with wrangler types, or building microservices.

GitHub
Install command
npx skhub add null-shot/workers
Markdown
SKILL.md

Cloudflare Workers

Essential patterns for building Cloudflare Workers applications with TypeScript, proper configuration, and Service Bindings for microservices.

FIRST: Project Setup

Initialize a new Workers project:

npm create cloudflare@latest my-worker
# OR
wrangler init my-worker

Minimal wrangler.jsonc:

{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2025-03-07",
  "compatibility_flags": ["nodejs_compat"],
  "observability": {
    "enabled": true,
    "head_sampling_rate": 1
  }
}

Code Standards

StandardRequirementNotes
LanguageTypeScript by defaultJavaScript only if explicitly requested
Module FormatES modules onlyNEVER use Service Worker format
ImportsAlways import types/classesMust import all used methods
File StructureSingle file unless specifiedKeep code in one file by default
DependenciesMinimize external depsUse official SDKs when available
Native BindingsNot supportedAvoid FFI/C bindings
TypesInclude TypeScript typesDefine Env interface for bindings

Handler Patterns

HTTP Request Handler (fetch)

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    
    // Route handling
    if (url.pathname === "/api/data") {
      return handleAPI(request, env);
    }
    
    return new Response("Hello World!", {
      headers: { "Content-Type": "text/plain" }
    });
  }
};

async function handleAPI(request: Request, env: Env): Promise<Response> {
  // Validate request method
  if (request.method !== "POST") {
    return new Response("Method not allowed", { status: 405 });
  }
  
  try {
    const data = await request.json();
    // Process data...
    return Response.json({ success: true, data });
  } catch (error) {
    return Response.json(
      { error: "Invalid JSON" },
      { status: 400 }
    );
  }
}

Scheduled Handler (cron)

export default {
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
    // Run scheduled tasks
    console.log("Cron triggered:", new Date(event.scheduledTime).toISOString());
    
    // Use waitUntil for background work
    ctx.waitUntil(performCleanup(env));
  }
};

async function performCleanup(env: Env): Promise<void> {
  // Background cleanup logic
  console.log("Cleanup completed");
}

wrangler.jsonc configuration:

{
  "triggers": {
    "crons": ["0 */6 * * *"]  // Every 6 hours
  }
}

Queue Consumer Handler

export default {
  async queue(batch: MessageBatch<QueueMessage>, env: Env, ctx: ExecutionContext): Promise<void> {
    for (const message of batch.messages) {
      try {
        await processMessage(message.body, env);
        message.ack();
      } catch (error) {
        console.error("Message processing failed:", error);
        message.retry();
      }
    }
  }
};

type QueueMessage = {
  id: string;
  data: unknown;
};

async function processMessage(body: QueueMessage, env: Env): Promise<void> {
  // Process queue message
  console.log("Processing message:", body.id);
}

Auto-Generate Environment Types

RECOMMENDED: Use wrangler types to automatically generate your Env interface from your wrangler.jsonc:

# Generate types from wrangler.jsonc
npx wrangler types

# Output to custom path
npx wrangler types ./types/env.d.ts

# Include runtime types (Wrangler >= 3.66.0)
npx wrangler types --experimental-include-runtime

This generates a worker-configuration.d.ts file with:

  • Env interface matching all your bindings (KV, R2, D1, secrets, etc.)
  • Runtime types matching your compatibility_date and compatibility_flags
  • Service binding types with full RPC method signatures

Add to tsconfig.json:

{
  "compilerOptions": {
    "types": ["@cloudflare/workers-types", "./worker-configuration"]
  }
}

When to regenerate:

  • After adding/removing bindings in wrangler.jsonc
  • After changing compatibility_date or compatibility_flags
  • After modifying .dev.vars (secrets)
  • Before deploying (run in CI/CD)

Example generated Env interface:

// worker-configuration.d.ts (auto-generated)
interface Env {
  // From wrangler.jsonc bindings
  MY_KV: KVNamespace;
  MY_BUCKET: R2Bucket;
  DB: D1Database;
  COUNTER: DurableObjectNamespace;
  AUTH_SERVICE: Service<typeof AuthService>;
  AI: Ai;
  MY_QUEUE: Queue;
  
  // From .dev.vars (secrets)
  DATABASE_URL: string;
  API_KEY: string;
  
  // From wrangler.jsonc vars
  ENVIRONMENT: "development" | "staging" | "production";
  API_VERSION: string;
}

Secrets Management

CRITICAL: Never put secrets in wrangler.jsonc! Secrets must be encrypted and hidden.

Secrets vs Environment Variables

TypeStorageUse ForVisibility
vars (wrangler.jsonc)PlaintextNon-sensitive config (URLs, flags)✅ Visible
secretsEncryptedAPI keys, passwords, tokens❌ Hidden

Local Development with .dev.vars

Create a .dev.vars file for local secrets (NEVER commit this file):

# .dev.vars (add to .gitignore)
DATABASE_URL="postgresql://localhost:5432/dev"
API_KEY="dev-key-12345"
STRIPE_SECRET="sk_test_..."

CI/CD Best Practice: Empty .dev.vars

For CI/CD and type generation, commit a .dev.vars with empty values:

# .dev.vars (committed to git)
# Real values set via: wrangler secret put
DATABASE_URL=""
API_KEY=""
STRIPE_SECRET=""

Why this works:

  • wrangler types reads .dev.vars to generate Env types
  • Empty values create correct TypeScript types
  • CI/CD can run type checking without real secrets
  • Production secrets are set via wrangler secret put or dashboard

Setting Production Secrets

Via Wrangler:

# Add/update secret (deploys immediately)
npx wrangler secret put API_KEY
# You'll be prompted for value

# List secrets (values never shown)
npx wrangler secret list

# Delete secret
npx wrangler secret delete API_KEY

Via Dashboard: Workers & Pages → Your Worker → Settings → Variables and Secrets → Add → Secret

Accessing secrets in code:

interface Env {
  DATABASE_URL: string;
  API_KEY: string;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Access secrets from env (same as regular env vars)
    const db = new Database(env.DATABASE_URL);
    
    // Validate API key
    const key = request.headers.get("x-api-key");
    if (key !== env.API_KEY) {
      return new Response("Unauthorized", { status: 401 });
    }
    
    return Response.json({ success: true });
  }
};

Secret Store (Account-Level Secrets)

For secrets shared across multiple Workers:

{
  "secrets_store_secrets": [
    {
      "binding": "SHARED_API_KEY",
      "store_id": "abc123def456",
      "secret_name": "GLOBAL_API_KEY"
    }
  ]
}

Accessing Secret Store:

interface Env {
  SHARED_API_KEY: {
    get(): Promise<string>;
  };
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Secret Store requires .get() call
    const apiKey = await env.SHARED_API_KEY.get();
    return Response.json({ success: true });
  }
};

See references/secrets.md for complete secrets management guide.

wrangler.jsonc Configuration

Complete example with common bindings:

{
  "name": "my-worker",
  "main": "src/index.ts",
  "compatibility_date": "2025-03-07",
  "compatibility_flags": ["nodejs_compat"],
  
  "observability": {
    "enabled": true,
    "head_sampling_rate": 1
  },
  
  "vars": {
    "ENVIRONMENT": "production"
  },
  
  "kv_namespaces": [
    { "binding": "MY_KV", "id": "your-kv-id" }
  ],
  
  "r2_buckets": [
    { "binding": "MY_BUCKET", "bucket_name": "my-bucket" }
  ],
  
  "d1_databases": [
    { "binding": "DB", "database_name": "my-db", "database_id": "your-db-id" }
  ],
  
  "durable_objects": {
    "bindings": [
      { "name": "COUNTER", "class_name": "Counter" }
    ]
  },
  
  "queues": {
    "producers": [
      { "binding": "MY_QUEUE", "queue": "my-queue" }
    ]
  }
}

Key Configuration Rules:

  • Use wrangler.jsonc, NOT wrangler.toml
  • Set compatibility_date to current date (format: YYYY-MM-DD)
  • Always include compatibility_flags: ["nodejs_compat"]
  • Enable observability with head_sampling_rate: 1 for full logging
  • Only include bindings that are actually used in your code
  • Never include npm dependencies in wrangler.jsonc

See references/configuration.md for complete configuration options.

Background Tasks with waitUntil

Offload non-critical work to run after the response is sent:

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // Return fast response
    const response = Response.json({ status: "accepted" });
    
    // Process in background (doesn't block response)
    ctx.waitUntil(performAsyncWork(request, env));
    
    return response;
  }
};

async function performAsyncWork(request: Request, env: Env): Promise<void> {
  // This runs after the response is sent
  const data = await request.json();
  await env.MY_KV.put("processed", JSON.stringify(data));
}

Use waitUntil for:

  • Analytics tracking
  • Cache warming
  • Logging
  • Non-critical database writes
  • Cleanup operations

Error Handling

HTTP Status Codes

async function handleRequest(request: Request, env: Env): Promise<Response> {
  try {
    // 400 - Bad Request
    if (!request.headers.get("content-type")) {
      return Response.json({ error: "Content-Type required" }, { status: 400 });
    }
    
    // 401 - Unauthorized
    const apiKey = request.headers.get("x-api-key");
    if (apiKey !== env.API_KEY) {
      return Response.json({ error: "Invalid API key" }, { status: 401 });
    }
    
    // 404 - Not Found
    const resource = await env.MY_KV.get("resource");
    if (!resource) {
      return Response.json({ error: "Resource not found" }, { status: 404 });
    }
    
    // 200 - Success
    return Response.json({ success: true, data: resource });
    
  } catch (error) {
    // 500 - Internal Server Error
    console.error("Request failed:", error);
    return Response.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}

Error Boundaries

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    try {
      return await handleRequest(request, env, ctx);
    } catch (error) {
      console.error("Unhandled error:", error);
      return Response.json(
        { 
          error: "An unexpected error occurred",
          message: error instanceof Error ? error.message : "Unknown error"
        },
        { status: 500 }
      );
    }
  }
};

Service Bindings (Microservices)

Service Bindings are the recommended way to build multi-Worker architectures. They enable Worker-to-Worker communication with zero latency, no HTTP overhead, and no additional costs.

Why Service Bindings?

BenefitDescription
Zero latencyBoth Workers run on same thread/server by default
No HTTP overheadDirect RPC calls, not HTTP requests
Zero additional costSplit functionality without increasing bills
Type-safe RPCCall methods with full TypeScript support
Internal-only WorkersBuild services not exposed to public internet
Independent deploymentEach Worker deploys on its own schedule

RPC Interface (Recommended)

Export an RPC class from your service Worker:

// auth-service/src/index.ts
import { WorkerEntrypoint } from "cloudflare:workers";

export class AuthService extends WorkerEntrypoint<Env> {
  async validateToken(token: string): Promise<{ valid: boolean; userId?: string }> {
    const userId = await this.env.AUTH_TOKENS.get(token);
    return { valid: !!userId, userId: userId || undefined };
  }
  
  async createToken(userId: string): Promise<string> {
    const token = crypto.randomUUID();
    await this.env.AUTH_TOKENS.put(token, userId, { expirationTtl: 86400 });
    return token;
  }
}

// Must also export default handler for HTTP access (if needed)
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    return new Response("Auth Service - use RPC interface");
  }
} satisfies ExportedHandler<Env>;

auth-service/wrangler.jsonc:

{
  "name": "auth-service",
  "main": "src/index.ts",
  "compatibility_date": "2025-03-07",
  "kv_namespaces": [
    { "binding": "AUTH_TOKENS", "id": "..." }
  ]
}

Calling the service from another Worker:

// api-worker/src/index.ts
interface Env {
  AUTH: Service<import("auth-service").AuthService>;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const token = request.headers.get("Authorization")?.slice(7);
    
    if (!token) {
      return new Response("Unauthorized", { status: 401 });
    }
    
    // Call RPC method directly
    const result = await env.AUTH.validateToken(token);
    
    if (!result.valid) {
      return new Response("Invalid token", { status: 401 });
    }
    
    return Response.json({ userId: result.userId });
  }
};

api-worker/wrangler.jsonc:

{
  "name": "api-worker",
  "main": "src/index.ts",
  "compatibility_date": "2025-03-07",
  "services": [
    {
      "binding": "AUTH",
      "service": "auth-service"
    }
  ]
}

Generate types for Service Bindings:

# In api-worker directory
npx wrangler types

This auto-generates the Env interface with the correct Service<AuthService> type.

Fetch-Based Service Binding

For simpler use cases or when you don't need RPC:

// api-worker/src/index.ts
interface Env {
  AUTH: Fetcher;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Forward request to auth service
    const authResponse = await env.AUTH.fetch(new Request("https://internal/validate", {
      method: "POST",
      body: JSON.stringify({ token: "..." })
    }));
    
    const result = await authResponse.json();
    return Response.json(result);
  }
};

See references/service-bindings.md for advanced patterns including environment-specific bindings and testing.

Detailed References

Best Practices

  1. Use TypeScript by default - Better type safety and IDE support
  2. Generate types with wrangler types - Auto-generate Env interface from config and .dev.vars
  3. NEVER put secrets in wrangler.jsonc - Use wrangler secret put or .dev.vars for local dev
  4. Use .dev.vars with empty values for CI - Enables type generation without exposing secrets
  5. Enable observability - Set observability.enabled: true for logging
  6. Use Service Bindings for microservices - Zero-cost, type-safe Worker-to-Worker calls
  7. Validate all inputs - Never trust user data
  8. Handle errors gracefully - Use try-catch and return appropriate status codes
  9. Use waitUntil for background tasks - Don't block response on non-critical work
  10. Keep bundle size small - Minimize dependencies for faster cold starts

Common Patterns

Complete API Worker

interface Env {
  MY_KV: KVNamespace;
  API_KEY: string;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    try {
      // CORS preflight
      if (request.method === "OPTIONS") {
        return new Response(null, {
          headers: {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
            "Access-Control-Allow-Headers": "Content-Type",
          }
        });
      }
      
      // Route handling
      const url = new URL(request.url);
      
      if (url.pathname === "/api/data" && request.method === "GET") {
        const data = await env.MY_KV.get("data");
        return Response.json({ data: data || null });
      }
      
      if (url.pathname === "/api/data" && request.method === "POST") {
        const body = await request.json();
        await env.MY_KV.put("data", JSON.stringify(body));
        return Response.json({ success: true });
      }
      
      return new Response("Not found", { status: 404 });
      
    } catch (error) {
      console.error("Request failed:", error);
      return Response.json(
        { error: "Internal server error" },
        { status: 500 }
      );
    }
  }
};

Middleware Pattern

type Middleware = (
  request: Request,
  env: Env,
  ctx: ExecutionContext,
  next: () => Promise<Response>
) => Promise<Response>;

const authMiddleware: Middleware = async (request, env, ctx, next) => {
  const apiKey = request.headers.get("x-api-key");
  if (apiKey !== env.API_KEY) {
    return new Response("Unauthorized", { status: 401 });
  }
  return next();
};

const loggingMiddleware: Middleware = async (request, env, ctx, next) => {
  console.log(`${request.method} ${request.url}`);
  const response = await next();
  console.log(`Response: ${response.status}`);
  return response;
};

function compose(...middlewares: Middleware[]) {
  return async (request: Request, env: Env, ctx: ExecutionContext): Promise<Response> => {
    let index = 0;
    
    const next = async (): Promise<Response> => {
      if (index >= middlewares.length) {
        return handleRequest(request, env);
      }
      const middleware = middlewares[index++];
      return middleware(request, env, ctx, next);
    };
    
    return next();
  };
}

export default {
  fetch: compose(loggingMiddleware, authMiddleware)
};

Resources

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.25

Published

Sep 25, 2026

Category

Uncategorized

License

Apache-2.0

Source path

workers

Default branch

main

Latest commit

cfdd76f

Tree SHA

a65de4a