Skip to main content

Guide

unenv vs edge-runtime vs @cloudflare/workers-types 2026

Compare unenv, edge-runtime, and @cloudflare/workers-types for edge and serverless environments. Node.js polyfills, edge runtime compatibility, WinterCG.

·PkgPulse Team·
0

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

Featureunenvedge-runtime@cloudflare/workers-types
PurposeNode.js polyfillsEdge emulatorTypeScript types
Runtime supportAny (build-time)Vercel EdgeCloudflare Workers
Polyfills✅ (30+ modules)N/AN/A
Local testingN/A❌ (use wrangler)
TypeScript types
KV/R2/D1 types
WinterCG compatible
Used byNitro, NuxtVercel, Next.jsCloudflare 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.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.