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
| Tool | Purpose | Install |
|---|---|---|
| Wrangler | CLI for everything (dev, deploy, manage resources) | npm install -D wrangler |
| C3 | create-cloudflare scaffolder | npm create cloudflare@latest |
| Vite plugin | Local dev with real bindings, production builds | npm install -D @cloudflare/vite-plugin |
| Vitest integration | Test Workers with real bindings | npm install -D @cloudflare/vitest-pool-workers |
| Miniflare | Local 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:
| Template | What you get |
|---|---|
hono | Hono API with TypeScript |
worker-typescript | Bare Worker with TypeScript |
worker | Bare Worker with JavaScript |
| Framework options | react, 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 dev | Vite plugin | |
|---|---|---|
| Frontend | None (API only) | Full HMR, React/Vue/etc. |
| Backend | workerd runtime | workerd via Miniflare |
| Config | wrangler.jsonc | wrangler.jsonc (automatic) |
| Port | 8787 | 5173 (Vite default) |
| Use case | API-only Workers | Full-stack apps |
| Build | wrangler deploy | vite 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
cloudflareTestplugin 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
workerdruntime (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
/__scheduledendpoint
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 devterminal (local development)
Source Maps
Wrangler uploads source maps automatically. Stack traces in production logs show your original TypeScript line numbers, not bundled JavaScript.
Recommended Workflow
- Scaffold with C3:
npm create cloudflare@latest - Develop with
wrangler dev(API) or Vite plugin (full-stack) - Test with Vitest +
@cloudflare/vitest-pool-workers - Generate types after config changes:
npx wrangler types - Deploy with
npx wrangler deploy(orvite build && wrangler deployfor full-stack) - Debug with
npx wrangler tail