Skip to main content

Miniflare vs Wrangler vs Workers SDK: Local Cloudflare Workers Dev (2026)

·PkgPulse Team

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 dev runs 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 dev is 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

Featurewranglerminiflare@cf/workers-types
Local dev server✅ (programmatic)
Deploy to Cloudflare
Unit testing API
KV simulationtypes only
D1 simulationtypes only
R2 simulationtypes 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"
  }
}

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 →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.