TL;DR
Wrangler is the official Cloudflare CLI for developing, testing, and deploying Workers — it's the entry point for everything Cloudflare Workers-related. Miniflare is the local Workers simulator that Wrangler uses internally — you can use it directly in tests via its API. @cloudflare/workers-types + @cloudflare/workers-sdk provide the TypeScript types and programmatic access to Workers features. In 2026: use wrangler dev for local development, use Miniflare directly for unit testing Workers, and use @cloudflare/workers-types for TypeScript type safety.
Key Takeaways
- wrangler: ~1M weekly downloads — official Cloudflare CLI,
wrangler devruns Workers locally - miniflare: ~300K weekly downloads — local Workers simulator, used by Wrangler internally
- @cloudflare/workers-types: ~3M weekly downloads — TypeScript types for the Workers runtime
- Wrangler 3+ embeds Miniflare —
wrangler devis powered by Miniflare under the hood - Miniflare simulates KV, D1, R2, Durable Objects, Queues locally — no actual Cloudflare account needed
- Workers run on V8 — not Node.js, so Node APIs (fs, path, etc.) are NOT available
The Cloudflare Workers Stack
Cloudflare Workers runtime:
- V8 JavaScript engine (NOT Node.js)
- Limited runtime: no fs, no child_process, no most Node built-ins
- 128MB memory limit, 10ms CPU per request (Bundled plan)
- Runs at the edge: 300+ locations worldwide
APIs available in Workers:
KV → key-value store (eventually consistent, global)
D1 → SQLite database (strong consistency, regional)
R2 → S3-compatible object storage (no egress fees)
Durable Objects → stateful objects with strict consistency
Queues → message queues with delivery guarantees
Hyperdrive → connection pooling for Postgres/MySQL
AI → inference API for AI models
Browser Rendering → headless browser API
Local development tools:
wrangler dev → serves worker locally with Miniflare
miniflare → programmatic Workers testing
Wrangler
Wrangler — the official Cloudflare Workers CLI:
Project setup
# Create a new Workers project:
npm create cloudflare@latest my-worker
# Or install wrangler and initialize:
npm install -D wrangler
npx wrangler init my-worker
# Project structure:
# src/index.ts — Worker code
# wrangler.toml — Configuration
# package.json
wrangler.toml
name = "pkgpulse-worker"
main = "src/index.ts"
compatibility_date = "2024-09-23"
node_compat = false # Disable Node.js compatibility layer
# KV namespace binding:
[[kv_namespaces]]
binding = "CACHE"
id = "abc123def456" # Production KV namespace ID
preview_id = "xyz789" # Local dev KV namespace (or auto-created)
# D1 database binding:
[[d1_databases]]
binding = "DB"
database_name = "pkgpulse"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# R2 bucket binding:
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "pkgpulse-assets"
# Environment variables (plain values — use secrets for sensitive):
[vars]
ENVIRONMENT = "production"
API_URL = "https://api.pkgpulse.com"
# Secrets (stored encrypted, set via CLI):
# wrangler secret put API_KEY
The Worker itself
// src/index.ts — exports a default fetch handler:
export interface Env {
// Bindings from wrangler.toml:
CACHE: KVNamespace
DB: D1Database
ASSETS: R2Bucket
API_KEY: string
ENVIRONMENT: string
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
// Route handler:
if (url.pathname === "/api/packages") {
return handlePackages(request, env, ctx)
}
if (url.pathname === "/api/health") {
return Response.json({ status: "ok", env: env.ENVIRONMENT })
}
return new Response("Not Found", { status: 404 })
},
}
async function handlePackages(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const url = new URL(request.url)
const name = url.searchParams.get("name")
if (!name) {
return Response.json({ error: "name is required" }, { status: 400 })
}
// Check KV cache:
const cached = await env.CACHE.get(`package:${name}`, { type: "json" })
if (cached) {
return Response.json(cached, {
headers: { "X-Cache": "HIT" },
})
}
// Query D1:
const result = await env.DB.prepare(
"SELECT * FROM packages WHERE name = ?"
).bind(name).first()
if (!result) {
return Response.json({ error: "Package not found" }, { status: 404 })
}
// Cache in KV (background, doesn't block response):
ctx.waitUntil(
env.CACHE.put(`package:${name}`, JSON.stringify(result), {
expirationTtl: 5 * 60, // 5 minutes
})
)
return Response.json(result)
}
Local development
# Start local dev server (powered by Miniflare):
npx wrangler dev
# Local dev features:
# - Hot reload on file changes
# - Simulates KV, D1, R2, Durable Objects locally
# - Available at http://localhost:8787
# - Remote mode (--remote) uses actual Cloudflare edge
# Run with live D1 data (remote):
npx wrangler dev --remote
# Deploy to production:
npx wrangler deploy
Miniflare
Miniflare — local Cloudflare Workers simulator:
Use Miniflare for testing
// tests/worker.test.ts — using Miniflare directly:
import { Miniflare } from "miniflare"
import { describe, test, expect, beforeAll, afterAll } from "vitest"
let mf: Miniflare
beforeAll(async () => {
mf = new Miniflare({
script: `
export default {
async fetch(request, env) {
const name = new URL(request.url).searchParams.get("name")
const pkg = await env.CACHE.get(name)
if (pkg) return Response.json(JSON.parse(pkg))
return new Response("Not Found", { status: 404 })
}
}
`,
modules: true, // Use ES modules
kvNamespaces: ["CACHE"], // Simulate KV
// d1Databases: ["DB"], // Simulate D1
// r2Buckets: ["ASSETS"], // Simulate R2
})
})
afterAll(async () => {
await mf.dispose()
})
test("returns cached package", async () => {
// Seed KV:
const kv = await mf.getKVNamespace("CACHE")
await kv.put("react", JSON.stringify({ name: "react", score: 92.5 }))
// Make a request:
const response = await mf.dispatchFetch("http://localhost/?name=react")
expect(response.status).toBe(200)
const data = await response.json()
expect(data.name).toBe("react")
expect(data.score).toBe(92.5)
})
test("returns 404 for unknown package", async () => {
const response = await mf.dispatchFetch("http://localhost/?name=nonexistent")
expect(response.status).toBe(404)
})
Loading from wrangler.toml
import { Miniflare } from "miniflare"
// Load Miniflare config directly from wrangler.toml:
const mf = new Miniflare({
scriptPath: "src/index.ts",
wranglerConfigPath: "wrangler.toml", // Use real wrangler config
modules: true,
// Override for tests:
kvNamespaces: ["CACHE"],
d1Databases: ["DB"],
})
D1 testing
import { Miniflare } from "miniflare"
const mf = new Miniflare({
modules: true,
d1Databases: ["DB"],
script: `
export default {
async fetch(request, env) {
const { results } = await env.DB.prepare(
"SELECT * FROM packages WHERE health_score > 80"
).all()
return Response.json(results)
}
}
`,
})
// Setup D1 schema and seed data:
const db = await mf.getD1Database("DB")
await db.exec(`
CREATE TABLE packages (id INTEGER PRIMARY KEY, name TEXT, health_score REAL);
INSERT INTO packages VALUES (1, 'react', 92.5);
INSERT INTO packages VALUES (2, 'vue', 89.0);
INSERT INTO packages VALUES (3, 'legacy', 45.0);
`)
const response = await mf.dispatchFetch("http://localhost/")
const data = await response.json()
// [{ id: 1, name: "react", health_score: 92.5 }, { id: 2, name: "vue", health_score: 89 }]
@cloudflare/workers-types
// Install TypeScript types for the Workers runtime:
// npm install -D @cloudflare/workers-types
// tsconfig.json:
{
"compilerOptions": {
"lib": ["es2022"],
"types": ["@cloudflare/workers-types/2023-07-01"] // Pin to a compat date
}
}
// Now you get proper types for KV, D1, R2, fetch, Request, Response, etc.:
const kv: KVNamespace = env.CACHE
const db: D1Database = env.DB
const bucket: R2Bucket = env.ASSETS
// KV types:
await kv.put("key", "value", { expirationTtl: 3600 })
const value: string | null = await kv.get("key")
// D1 types:
const stmt: D1PreparedStatement = db.prepare("SELECT * FROM packages")
const result: D1Result<{ name: string; score: number }> = await stmt.all()
Feature Comparison
| Feature | wrangler | miniflare | @cf/workers-types |
|---|---|---|---|
| Local dev server | ✅ | ✅ (programmatic) | ❌ |
| Deploy to Cloudflare | ✅ | ❌ | ❌ |
| Unit testing API | ❌ | ✅ | ❌ |
| KV simulation | ✅ | ✅ | types only |
| D1 simulation | ✅ | ✅ | types only |
| R2 simulation | ✅ | ✅ | types only |
| TypeScript types | ❌ | ❌ | ✅ |
| Hot reload | ✅ | ❌ | ❌ |
| Secrets management | ✅ | ❌ | ❌ |
| Weekly downloads | ~1M | ~300K | ~3M |
When to Use Each
Use wrangler for:
- Local development (
wrangler dev) - Deploying to Cloudflare (
wrangler deploy) - Managing secrets, KV namespaces, D1 databases (
wrangler kv,wrangler d1) - Generating type definitions (
wrangler types)
Use Miniflare for:
- Unit testing Workers in Vitest/Jest
- Integration tests that need simulated KV/D1/R2
- CI environments (no actual Cloudflare account needed)
Use @cloudflare/workers-types for:
- TypeScript type safety in your Worker code
- Autocomplete for
KVNamespace,D1Database,R2Bucket,ExecutionContext
All three together (recommended setup):
{
"devDependencies": {
"wrangler": "^3.0.0",
"miniflare": "^3.0.0",
"@cloudflare/workers-types": "^4.0.0",
"vitest": "^2.0.0"
}
}
Understanding the V8 Isolate Model
Cloudflare Workers run in V8 isolates rather than full Node.js processes, and this architectural difference has profound implications for how you write and test Worker code. A V8 isolate is a lightweight, sandboxed JavaScript execution context that starts in under a millisecond — orders of magnitude faster than spinning up a Node.js process or a container. Each Worker request runs in its own isolate, which means there is no shared mutable state between requests unless you explicitly use Durable Objects or KV storage.
This isolate model is why the Workers runtime exposes a different set of APIs than Node.js. fs, path, child_process, net, and most of the Node.js standard library are unavailable because they model OS-level abstractions that do not exist in the isolate sandbox. What you get instead are web-standard APIs: fetch, Request, Response, ReadableStream, WebSocket, crypto, and TextEncoder. The node_compat flag in wrangler.toml enables a compatibility shim that polyfills some Node.js APIs (like Buffer, process, EventEmitter), but this is a compatibility layer, not the real thing. Production Workers are best written using web-standard APIs from the start.
Local Development Fidelity with Miniflare
One of the persistent pain points in Workers development before Miniflare was the round-trip to production to test KV operations. Miniflare solves this by implementing a complete in-process simulation of the Workers runtime APIs. The KV simulation uses SQLite under the hood, the D1 simulation uses SQLite directly (D1 is SQLite on Cloudflare), and the R2 simulation stores objects in the local filesystem. This means wrangler dev can run your Worker with full KV/D1/R2 functionality without making any network requests to Cloudflare's infrastructure.
The fidelity is high but not perfect. Miniflare's KV simulation is eventually consistent in the API shape but strongly consistent in practice — there are no actual replication delays in local mode. This can mask bugs that only appear in production when a KV write in one edge location hasn't propagated to another edge location where a subsequent request lands. For development and unit testing, this is acceptable. For integration tests that specifically need to validate eventual consistency behavior, you need Wrangler's --remote mode which routes requests through actual Cloudflare infrastructure.
TypeScript Types and the Compatibility Date System
The @cloudflare/workers-types package is not a simple type declaration file — it is versioned according to Cloudflare's compatibility date system. Each compatibility_date in your wrangler.toml corresponds to a specific set of runtime APIs and behaviors. Types pinned to "@cloudflare/workers-types/2023-07-01" will only include APIs that were stable as of that date, protecting you from accidentally using newer APIs that might not be available in older runtimes.
When you run wrangler types, Wrangler generates a worker-configuration.d.ts file that derives the Env interface types from your wrangler.toml bindings. This auto-generation eliminates a common source of bugs where the TypeScript type for env.CACHE diverges from the actual KV namespace binding configured in wrangler.toml. The generated file gets imported automatically by your Worker's TypeScript configuration, giving you accurate types for every binding without manual maintenance.
Durable Objects: The Most Powerful and Complex Binding
Durable Objects deserve special attention because they fundamentally change what is possible in a serverless Worker architecture. A Durable Object is a JavaScript class with both persistent storage and execution guarantees — only one instance of a given Durable Object can run at a time globally, and all requests to that object are serialized through a single-threaded event loop. This makes it possible to build strongly consistent systems on Cloudflare's globally distributed infrastructure without a traditional database.
In Miniflare, Durable Objects are simulated in the same process as your Worker, which means they behave correctly for most testing purposes — you can write, read, and use the Durable Object's WebSocket hibernation API in tests. The production behavior difference is that in production, a Durable Object instance lives in a specific Cloudflare data center (the one closest to where it was first created), and all subsequent requests to that object are routed there. In Miniflare, there is no such routing — all objects live in memory. Tests that check geographic co-location behavior need the --remote flag.
Security Considerations for Workers
Workers run user code in a sandboxed V8 isolate, but security still requires deliberate design choices. Secrets stored in plain vars in wrangler.toml are visible in the Wrangler configuration file and in source control unless you gitignore the file. The correct approach is to use wrangler secret put for sensitive values — these are encrypted at rest in Cloudflare's system and injected at runtime without appearing in any file.
Environment variable naming is also a security consideration. Workers expose env.* bindings to the Worker's fetch handler, which means anything in env is accessible to any code running in your Worker bundle. If your Worker bundles third-party dependencies, those dependencies share the same env object. Treat every binding value as potentially visible to your entire dependency tree and scope secret access to the minimum necessary code paths. For Workers that handle user authentication, consider using Cloudflare Access or Workers Access Service Tokens rather than storing auth secrets in Worker environment variables.
Deployment and CI/CD Integration
Wrangler's deployment model integrates cleanly with GitHub Actions and other CI systems. The wrangler deploy command authenticates via CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID environment variables, making it straightforward to set up automated deployments. The --dry-run flag validates your Worker bundle and wrangler.toml configuration without uploading, useful for validating pull requests before merge.
For monorepos with multiple Workers, Wrangler supports --config to point to a specific wrangler.toml file, and the workspaces pattern allows sharing schema definitions or utility modules between Workers. Teams running many Workers benefit from Wrangler's --env flag, which allows different configurations for staging and production environments within a single wrangler.toml using the [env.staging] syntax.
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on wrangler v3.x, miniflare v3.x, and @cloudflare/workers-types v4.x.
Compare edge computing and serverless packages on PkgPulse →
See also: pm2 vs node:cluster vs tsx watch and h3 vs polka vs koa 2026, better-sqlite3 vs libsql vs sql.js.