unenv vs edge-runtime vs @cloudflare/workers-types: Edge Environment Polyfills (2026)
TL;DR
unenv is the UnJS environment polyfill layer — provides Node.js API polyfills for edge/browser runtimes, powers Nitro's cross-platform deployment, converts Node.js code to run anywhere. edge-runtime is Vercel's edge runtime emulator — local development environment matching Vercel Edge Functions, tests edge code locally, WinterCG compatible. @cloudflare/workers-types provides TypeScript types for Cloudflare Workers — types for KV, R2, D1, Durable Objects, and Workers runtime APIs. In 2026: unenv for universal polyfills, edge-runtime for Vercel edge testing, @cloudflare/workers-types for Cloudflare Workers TypeScript.
Key Takeaways
- unenv: ~5M weekly downloads — UnJS, Node.js polyfills for edge, powers Nitro
- edge-runtime: ~1M weekly downloads — Vercel, local edge emulator, WinterCG
- @cloudflare/workers-types: ~500K weekly downloads — TypeScript types for Workers APIs
- Different purposes: polyfill (unenv), emulate (edge-runtime), type (workers-types)
- unenv makes Node.js code portable to edge environments
- edge-runtime lets you test Vercel Edge Functions locally
unenv
unenv — universal environment polyfills:
What it does
unenv converts Node.js built-in modules to work in non-Node environments:
Node.js module → unenv polyfill
─────────────────────────────────────
node:buffer → Buffer polyfill (using Uint8Array)
node:crypto → Web Crypto API wrapper
node:events → EventEmitter polyfill
node:fs → No-op or in-memory filesystem
node:http → Fetch-based HTTP polyfill
node:path → Pure JS path implementation
node:process → Minimal process shim
node:stream → Web Streams adapter
node:url → URL/URLSearchParams polyfill
node:util → Utility polyfills
When a Node.js API has no edge equivalent, unenv provides
either a no-op stub or throws a helpful error.
How Nitro uses unenv
// nitro.config.ts — unenv is built into Nitro:
export default defineNitroConfig({
// Nitro automatically applies unenv polyfills per deploy target:
preset: "cloudflare-pages",
// → unenv polyfills node:crypto, node:buffer, node:events
// → Replaces node:fs with no-op (no filesystem on edge)
preset: "vercel-edge",
// → Similar polyfills for Vercel Edge Runtime
preset: "node-server",
// → No polyfills needed (native Node.js)
})
// Your server code just uses Node.js APIs:
import { createHash } from "node:crypto"
import { Buffer } from "node:buffer"
export default defineEventHandler(() => {
const hash = createHash("sha256").update("hello").digest("hex")
const buf = Buffer.from("hello", "utf-8")
return { hash, buf: buf.toString("base64") }
})
// Works on Node.js, Cloudflare Workers, Vercel Edge, Deno Deploy
Programmatic usage
import { env } from "unenv"
// Get environment config for a target:
const cloudflareEnv = env("cloudflare")
const vercelEdgeEnv = env("vercel-edge")
// Each env provides:
console.log(cloudflareEnv.alias)
// → {
// "node:crypto": "unenv/runtime/node/crypto",
// "node:buffer": "unenv/runtime/node/buffer",
// "node:events": "unenv/runtime/node/events",
// ...
// }
console.log(cloudflareEnv.inject)
// → {
// process: "unenv/runtime/node/process",
// Buffer: ["unenv/runtime/node/buffer", "Buffer"],
// ...
// }
// Use with bundlers (Rollup, Webpack, Vite):
// These aliases redirect imports to polyfills at build time
Individual polyfills
// Use individual polyfills in your own code:
import { Buffer } from "unenv/runtime/node/buffer"
import { EventEmitter } from "unenv/runtime/node/events"
import { createHash } from "unenv/runtime/node/crypto"
// These work in browsers, Deno, Cloudflare Workers, etc.
const emitter = new EventEmitter()
emitter.on("data", (msg) => console.log(msg))
emitter.emit("data", "Hello from edge!")
const hash = createHash("sha256").update("test").digest("hex")
edge-runtime
edge-runtime — Vercel edge emulator:
Local development
import { EdgeRuntime } from "edge-runtime"
// Create a local edge runtime:
const runtime = new EdgeRuntime()
// Evaluate code in edge context:
const result = await runtime.evaluate(`
const response = new Response("Hello from edge!")
response.text()
`)
console.log(result) // → "Hello from edge!"
Testing edge functions
import { EdgeRuntime, runServer } from "edge-runtime"
// Create runtime with your edge function:
const runtime = new EdgeRuntime({
initialCode: `
addEventListener("fetch", (event) => {
event.respondWith(
new Response(JSON.stringify({ message: "Hello!" }), {
headers: { "content-type": "application/json" },
})
)
})
`,
})
// Run as local HTTP server:
const server = await runServer({ runtime, port: 3000 })
// Test with fetch:
const response = await fetch("http://localhost:3000")
const data = await response.json()
console.log(data) // → { message: "Hello!" }
await server.close()
Available APIs
edge-runtime provides WinterCG-compatible APIs:
Web APIs:
✅ fetch, Request, Response, Headers
✅ URL, URLSearchParams, URLPattern
✅ TextEncoder, TextDecoder
✅ ReadableStream, WritableStream, TransformStream
✅ AbortController, AbortSignal
✅ structuredClone
✅ crypto (Web Crypto API)
✅ atob, btoa
✅ setTimeout, setInterval
✅ console
NOT available (by design):
❌ node:fs (no filesystem)
❌ node:net (no TCP sockets)
❌ node:child_process (no subprocesses)
❌ eval(), new Function() (no dynamic code)
❌ __dirname, __filename (no filesystem)
With Jest/Vitest
// vitest.config.ts — test edge functions locally:
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
environment: "edge-runtime",
},
})
// __tests__/api.test.ts
describe("Edge API", () => {
it("returns JSON response", async () => {
const request = new Request("https://example.com/api")
const response = await handleRequest(request)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.message).toBe("Hello!")
})
})
@cloudflare/workers-types
@cloudflare/workers-types — TypeScript types:
Setup
// tsconfig.json
{
"compilerOptions": {
"types": ["@cloudflare/workers-types"]
}
}
// Or with compatibility flags:
// tsconfig.json
{
"compilerOptions": {
"types": ["@cloudflare/workers-types/2024-01-01"]
}
}
Workers handler types
// src/index.ts
export interface Env {
MY_KV: KVNamespace
MY_R2: R2Bucket
MY_DB: D1Database
MY_DO: DurableObjectNamespace
API_KEY: string // Secret
}
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const url = new URL(request.url)
if (url.pathname === "/api/data") {
// KV:
const cached = await env.MY_KV.get("key")
if (cached) return new Response(cached)
// D1:
const { results } = await env.MY_DB
.prepare("SELECT * FROM packages LIMIT 10")
.all()
const json = JSON.stringify(results)
await env.MY_KV.put("key", json, { expirationTtl: 3600 })
return new Response(json, {
headers: { "content-type": "application/json" },
})
}
return new Response("Not found", { status: 404 })
},
}
KV types
// KVNamespace provides typed methods:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Get:
const value = await env.MY_KV.get("key") // string | null
const json = await env.MY_KV.get("key", "json") // any | null
const buffer = await env.MY_KV.get("key", "arrayBuffer") // ArrayBuffer | null
const stream = await env.MY_KV.get("key", "stream") // ReadableStream | null
// Get with metadata:
const { value: val, metadata } = await env.MY_KV.getWithMetadata<{
createdAt: string
}>("key")
// Put:
await env.MY_KV.put("key", "value")
await env.MY_KV.put("key", JSON.stringify(data), {
expirationTtl: 3600,
metadata: { createdAt: new Date().toISOString() },
})
// List:
const list = await env.MY_KV.list({ prefix: "user:" })
// → { keys: [{ name: "user:1", ... }], list_complete: boolean }
// Delete:
await env.MY_KV.delete("key")
return new Response("OK")
},
}
R2 and D1 types
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// R2 (Object Storage):
const object = await env.MY_R2.get("images/photo.jpg")
if (object) {
return new Response(object.body, {
headers: {
"content-type": object.httpMetadata?.contentType ?? "application/octet-stream",
"etag": object.httpEtag,
},
})
}
await env.MY_R2.put("images/upload.jpg", request.body, {
httpMetadata: { contentType: "image/jpeg" },
})
// D1 (SQL Database):
const { results } = await env.MY_DB
.prepare("SELECT * FROM packages WHERE name = ?")
.bind("react")
.all<{ name: string; downloads: number }>()
// → results: Array<{ name: string; downloads: number }>
const row = await env.MY_DB
.prepare("SELECT count(*) as total FROM packages")
.first<{ total: number }>()
// → row: { total: number } | null
return Response.json(results)
},
}
Feature Comparison
| Feature | unenv | edge-runtime | @cloudflare/workers-types |
|---|---|---|---|
| Purpose | Node.js polyfills | Edge emulator | TypeScript types |
| Runtime support | Any (build-time) | Vercel Edge | Cloudflare Workers |
| Polyfills | ✅ (30+ modules) | N/A | N/A |
| Local testing | N/A | ✅ | ❌ (use wrangler) |
| TypeScript types | ❌ | ❌ | ✅ |
| KV/R2/D1 types | ❌ | ❌ | ✅ |
| WinterCG compatible | ✅ | ✅ | ✅ |
| Used by | Nitro, Nuxt | Vercel, Next.js | Cloudflare Workers |
| Weekly downloads | ~5M | ~1M | ~500K |
When to Use Each
Use unenv if:
- Building code that runs on Node.js, edge, and browsers
- Using Nitro or Nuxt for cross-platform deployment
- Need Node.js API polyfills for edge runtimes
- Building a framework that targets multiple environments
Use edge-runtime if:
- Testing Vercel Edge Functions locally
- Need a WinterCG-compatible test environment
- Building Next.js middleware or edge routes
- Want to validate code runs in edge constraints
Use @cloudflare/workers-types if:
- Building Cloudflare Workers in TypeScript
- Need types for KV, R2, D1, Durable Objects
- Want autocomplete for Workers runtime APIs
- Using wrangler for local development
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on unenv v1.x, edge-runtime v3.x, and @cloudflare/workers-types v4.x.