Dev Tooling

The Cloudflare developer toolchain covers scaffolding, local development, testing, and deployment. This deep-dive covers every tool and how they fit together.

Prerequisites: First Worker

The Toolchain

ToolPurposeInstall
WranglerCLI for everything (dev, deploy, manage resources)npm install -D wrangler
C3create-cloudflare scaffoldernpm create cloudflare@latest
Vite pluginLocal dev with real bindings, production buildsnpm install -D @cloudflare/vite-plugin
Vitest integrationTest Workers with real bindingsnpm install -D @cloudflare/vitest-pool-workers
MiniflareLocal workerd simulation (powers Wrangler dev and Vite)Bundled with Wrangler

Wrangler 4.x

Wrangler is the primary CLI. It manages your Worker lifecycle and every Cloudflare service.

Core Commands

# Development
npx wrangler dev                 # Start local dev server
npx wrangler dev --remote        # Dev against real Cloudflare services

# Deployment
npx wrangler deploy              # Deploy to production
npx wrangler versions upload     # Upload without activating
npx wrangler versions deploy     # Activate a specific version

# Debugging
npx wrangler tail                # Stream live logs from production
npx wrangler tail --format=json  # JSON-formatted log output

# Type generation
npx wrangler types               # Generate TypeScript types from bindings

# Resource management
npx wrangler d1 list                          # List D1 databases
npx wrangler d1 execute <db> --command="SQL"  # Run SQL
npx wrangler d1 migrations apply <db>         # Apply migrations
npx wrangler r2 bucket list                   # List R2 buckets
npx wrangler kv key list --binding=CACHE      # List KV keys
npx wrangler queues list                      # List queues

# Secrets
npx wrangler secret put SECRET_NAME           # Set a secret (prompts for value)
npx wrangler secret list                      # List secrets
npx wrangler secret delete SECRET_NAME        # Remove a secret

Comprehensive wrangler.jsonc

Here is a complete wrangler.jsonc showing every binding type and configuration option:

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "webhook-hub",
  "main": "src/index.ts",
  "compatibility_date": "2025-04-01",
  "compatibility_flags": ["nodejs_compat"],

  // Worker limits (paid plan)
  "limits": {
    "cpu_ms": 30000
  },

  // Static assets
  "assets": {
    "directory": "./dist/client",
    "binding": "ASSETS",
    "not_found_handling": "single-page-application",
    "run_worker_first": ["/api/*"]
  },

  // D1 databases
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "webhook-hub-db",
      "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "migrations_dir": "migrations"
    }
  ],

  // R2 buckets
  "r2_buckets": [
    {
      "binding": "BUCKET",
      "bucket_name": "webhook-hub-storage"
    }
  ],

  // KV namespaces
  "kv_namespaces": [
    {
      "binding": "CACHE",
      "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
  ],

  // Queues
  "queues": {
    "producers": [
      {
        "binding": "DELIVERY_QUEUE",
        "queue": "webhook-deliveries"
      }
    ],
    "consumers": [
      {
        "queue": "webhook-deliveries",
        "max_batch_size": 10,
        "max_batch_timeout": 30,
        "max_retries": 3,
        "dead_letter_queue": "webhook-deliveries-dlq",
        "retry_delay": 60
      }
    ]
  },

  // Durable Objects
  "durable_objects": {
    "bindings": [
      {
        "name": "DELIVERY_TRACKER",
        "class_name": "DeliveryTracker",
        "sqlite_database": true
      }
    ]
  },

  // DO migrations
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["DeliveryTracker"]
    }
  ],

  // Workflows
  "workflows": [
    {
      "name": "webhook-delivery",
      "binding": "WEBHOOK_WORKFLOW",
      "class_name": "WebhookDeliveryWorkflow"
    }
  ],

  // Hyperdrive
  "hyperdrive": [
    {
      "binding": "HYPERDRIVE",
      "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    }
  ],

  // Browser rendering
  "browser": {
    "binding": "BROWSER"
  },

  // Cron triggers
  "triggers": {
    "crons": ["*/5 * * * *", "0 0 * * *"]
  },

  // Environment variables (non-secret)
  "vars": {
    "ENVIRONMENT": "production",
    "LOG_LEVEL": "info"
  },

  // Service bindings (Worker-to-Worker)
  "services": [
    {
      "binding": "AUTH_SERVICE",
      "service": "auth-worker"
    }
  ]
}

After editing wrangler.jsonc, always regenerate types:

npx wrangler types

This updates worker-configuration.d.ts so c.env stays fully typed.

C3: create-cloudflare

The official scaffolder for new projects:

# Interactive mode
npm create cloudflare@latest

# With options
npm create cloudflare@latest my-project -- --template=hono
npm create cloudflare@latest my-app -- --framework=react
npm create cloudflare@latest my-api -- --template=worker-typescript

Available templates:

TemplateWhat you get
honoHono API with TypeScript
worker-typescriptBare Worker with TypeScript
workerBare Worker with JavaScript
Framework optionsreact, next, astro, remix, nuxt, svelte, etc.

C3 sets up the project structure, wrangler.jsonc, TypeScript config, and installs dependencies. It also optionally deploys on creation.

Vite Plugin

The @cloudflare/vite-plugin replaces wrangler dev for full-stack apps. It runs your Worker in a local workerd runtime with real bindings (D1, R2, KV, etc.) while Vite handles the frontend with HMR. For framework-specific integration details, see the Vite Plugin deep-dive in the frameworks topic.

Setup

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";

export default defineConfig({
  plugins: [react(), cloudflare()],
});

That’s it. The plugin reads wrangler.jsonc automatically. No duplicate configuration needed.

How It Differs from wrangler dev

wrangler devVite plugin
FrontendNone (API only)Full HMR, React/Vue/etc.
Backendworkerd runtimeworkerd via Miniflare
Configwrangler.jsoncwrangler.jsonc (automatic)
Port87875173 (Vite default)
Use caseAPI-only WorkersFull-stack apps
Buildwrangler deployvite build && wrangler deploy

Use wrangler dev for API-only Workers. Use the Vite plugin when you have a frontend.

Local Bindings

Both wrangler dev and the Vite plugin provide real, local implementations of Cloudflare services via Miniflare:

  • D1: local SQLite database (persistent across restarts)
  • R2: local file-backed object storage
  • KV: local key-value store
  • Queues: in-memory queue processing
  • Durable Objects: local DO instances with storage

Your code runs against real implementations, not mocks. If it works locally, it works deployed.

Vitest Integration

Test Workers with real bindings using @cloudflare/vitest-pool-workers. Requires Vitest 4.1+.

Setup

npm install -D vitest @cloudflare/vitest-pool-workers
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { cloudflareTest } from "@cloudflare/vitest-pool-workers";

export default defineConfig({
  plugins: [cloudflareTest()],
  test: {
    pool: "@cloudflare/vitest-pool-workers",
  },
});

Gotcha: Use the cloudflareTest plugin from @cloudflare/vitest-pool-workers. Do not use the old pool-based configuration from older tutorials. The plugin approach is the current recommended setup and requires Vitest 4.1+.

Writing Tests

// src/index.test.ts
import { describe, it, expect } from "vitest";
import { env } from "cloudflare:test";
import app from "./index";

describe("Webhook Hub API", () => {
  it("returns 200 on health check", async () => {
    const res = await app.fetch(
      new Request("http://localhost/health"),
      env
    );
    expect(res.status).toBe(200);
    expect(await res.text()).toBe("ok");
  });

  it("stores a webhook in D1", async () => {
    const res = await app.fetch(
      new Request("http://localhost/webhook/github", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ event: "push" }),
      }),
      env
    );
    expect(res.status).toBe(201);

    // Verify it's in D1
    const row = await env.DB.prepare(
      "SELECT * FROM webhooks WHERE source = 'github'"
    ).first();
    expect(row).toBeTruthy();
  });

  it("rate limits excessive requests", async () => {
    // Seed KV with a high count
    await env.CACHE.put("rate:test-source", "101");

    const res = await app.fetch(
      new Request("http://localhost/webhook/test-source", {
        method: "POST",
        body: "{}",
      }),
      env
    );
    expect(res.status).toBe(429);
  });
});

Tests run inside a real workerd runtime with real bindings. env from cloudflare:test gives you access to all bindings declared in wrangler.jsonc.

Running Tests

npx vitest           # Watch mode
npx vitest run       # Single run
npx vitest run --reporter=verbose  # Detailed output

Miniflare

Miniflare is the local simulation engine that powers both wrangler dev and the Vite plugin. You rarely interact with it directly, but understanding what it does helps debug issues.

What Miniflare Provides

  • Runs the actual workerd runtime (the same binary used in production)
  • Simulates all Cloudflare bindings locally (D1, R2, KV, DO, Queues)
  • Persists local data in .wrangler/state/ by default
  • Handles cron trigger simulation via /__scheduled endpoint

Local Data Persistence

# Local D1 data lives here
.wrangler/state/v3/d1/

# Local KV data
.wrangler/state/v3/kv/

# Local R2 data
.wrangler/state/v3/r2/

To reset local state:

rm -rf .wrangler/state/

Cron Testing

Test cron handlers locally by hitting the /__scheduled endpoint:

# Trigger a specific cron expression
curl "http://localhost:8787/__scheduled?cron=*/5+*+*+*+*"

The expression must be URL-encoded and match one declared in wrangler.jsonc.

Debugging

wrangler tail

Stream live logs from a deployed Worker:

# Plain text logs
npx wrangler tail

# JSON format (good for piping to jq)
npx wrangler tail --format=json

# Filter by status
npx wrangler tail --status=error

# Filter by IP or method
npx wrangler tail --ip=203.0.113.1
npx wrangler tail --method=POST

console.log in Workers

All console.log, console.error, etc. output appears in:

  • wrangler tail (live streaming)
  • Workers dashboard (Logs tab)
  • wrangler dev terminal (local development)

Source Maps

Wrangler uploads source maps automatically. Stack traces in production logs show your original TypeScript line numbers, not bundled JavaScript.

  1. Scaffold with C3: npm create cloudflare@latest
  2. Develop with wrangler dev (API) or Vite plugin (full-stack)
  3. Test with Vitest + @cloudflare/vitest-pool-workers
  4. Generate types after config changes: npx wrangler types
  5. Deploy with npx wrangler deploy (or vite build && wrangler deploy for full-stack)
  6. Debug with npx wrangler tail