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
Production Architecture for Edge-First Applications
Deploying applications to edge runtimes requires rethinking several assumptions that hold in Node.js. The most consequential constraint is that edge workers have a CPU time limit (typically 50ms per request on Cloudflare Workers, though this can be extended with Unbound Workers) and no persistent local state between requests — every invocation starts fresh. This means database connections cannot be held open across requests, which is why edge-compatible databases (Neon, PlanetScale, Cloudflare D1) use HTTP-based query protocols rather than persistent TCP connections. The unenv polyfills for Node.js built-ins like node:crypto and node:buffer work within these constraints because they use the Web Crypto API under the hood rather than native Node.js bindings. When building Nitro-based applications for edge targets, the key production concern is auditing your dependency tree for packages that use Node.js APIs with no edge equivalent — unenv will stub them, but stubbed APIs that throw at runtime rather than build time can cause production failures that are difficult to detect without thorough edge-specific testing.
TypeScript Configuration for Multiple Edge Targets
When your codebase targets multiple deployment environments — say, Cloudflare Workers for API routes, Vercel Edge for middleware, and a Node.js server for background jobs — TypeScript configuration becomes a significant concern. The @cloudflare/workers-types package adds Cloudflare-specific globals like KVNamespace, R2Bucket, and DurableObjectNamespace to the TypeScript environment, but these types conflict with standard Node.js or browser type definitions. The recommended approach is to scope Workers types to specific files using a triple-slash directive (/// <reference types="@cloudflare/workers-types" />) rather than adding them globally in tsconfig.json, which prevents type pollution into files meant for other environments. For monorepos that deploy to both Cloudflare Workers and a Node.js server, a common pattern is to separate the Workers handlers into their own TypeScript project with a dedicated tsconfig.json that references workers types, while the shared business logic lives in a package with no platform-specific type references.
Testing Edge Code Locally and in CI
Testing code that runs in edge environments locally requires matching the runtime constraints. The edge-runtime package serves this purpose for Vercel-targeted code, providing a Node.js-based evaluation environment that enforces the same API surface and rejects disallowed globals. For Cloudflare Workers, Miniflare (the local simulator bundled into wrangler) provides equivalent local testing with full KV, R2, D1, and Durable Objects simulation. In CI pipelines, a common pattern is to run the full test suite against both the edge-runtime environment and the standard Node.js environment, using Vitest's environment option to switch between them. This catches the class of bugs where code works locally in Node.js but fails on the edge because it uses a forbidden API — fs.readFileSync, process.env access patterns that don't match the Workers globals, or synchronous crypto operations that need to be await-ed in the Web Crypto API.
Security Considerations in Edge Workers
Edge workers run in shared infrastructure, which introduces security considerations specific to multi-tenant environments. Cloudflare Workers run in V8 isolates, which provide strong isolation between customer workloads without the overhead of full containers. However, this means you cannot rely on process-level isolation or file system access for security boundaries — all security must be expressed through your application logic. The @cloudflare/workers-types package's Env interface pattern is important here: secrets should always be accessed through the env parameter passed to the handler rather than hardcoded or loaded from files that don't exist in the edge environment. Wrangler's --secret command stores secrets encrypted and injects them into the env object at runtime, following the same pattern as environment variables in traditional deployments. For unenv-powered Nitro deployments to Cloudflare Pages or Workers, environment variables are configured through the Cloudflare dashboard or wrangler.toml and accessed via the injected process.env shim that unenv provides.
Performance Characteristics and Cold Start
Cold start performance is a critical metric for edge and serverless deployments that affects user-facing latency. Cloudflare Workers using V8 isolates have essentially zero cold start time in production because isolates start in milliseconds and Cloudflare pre-warms them across its edge locations. Vercel Edge Functions using the edge-runtime have similarly fast cold starts because they also run in V8 isolates. The performance comparison that matters for choosing between edge targets and traditional serverless (Lambda, Cloud Run) is initialization cost: a Node.js Lambda function that imports heavy dependencies (like an ORM or PDF generator) can have 500ms+ cold starts, while an edge worker that can't import those dependencies at all effectively has sub-10ms initialization. The practical implication is that edge workers are best suited for lightweight, latency-sensitive request processing — authentication, routing, A/B testing, localization — while heavier computations belong in background workers or traditional serverless functions that tolerate cold start.
Debugging Edge Workers Across Environments
Debugging edge code is meaningfully harder than debugging traditional Node.js applications because breakpoints, full stack traces, and local file system access are unavailable or limited in production. Cloudflare Workers provides wrangler dev for local development, which runs workers in a local V8 isolate using Miniflare under the hood — this replicates the Cloudflare production environment closely enough that most issues are caught locally. Vercel's edge-runtime provides a createEdgeRuntimeSandbox API for testing edge function behavior in Jest or Vitest without a full deployment, which is useful for unit testing route handlers and middleware before deployment. For unenv-based testing, Nitro's local development server emulates the target deployment environment locally, but behavior differences between local Node.js emulation and the actual edge target can still cause surprises, especially for Web Crypto API and fetch compatibility edge cases. Structured logging via console.log with JSON output is the primary debugging tool in deployed edge workers — both Cloudflare's Workers dashboard and Vercel's deployment logs capture this output and allow filtering by request ID.
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.
Compare edge tooling and serverless utilities on PkgPulse →
See also: ipx vs @vercel/og vs satori 2026 and pg vs postgres.js vs @neondatabase/serverless, acorn vs @babel/parser vs espree.