universal-javascript-runtimes
Universal JavaScript Runtimes
Overview
Modern JavaScript can run in many environments: Node.js, Deno, Bun, Cloudflare Workers, browsers, and more. Universal JavaScript means writing code once that runs everywhere.
The Runtime Fragmentation Problem
JAVASCRIPT RUNTIMES (2024):
┌─────────────────────────────────────────────────────────────────┐
│ Traditional Servers │
├─────────────────────────────────────────────────────────────────┤
│ Node.js │ The original server runtime (2009) │
│ Deno │ Secure runtime by Node creator (2020) │
│ Bun │ Fast all-in-one runtime (2022) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Edge/Serverless │
├─────────────────────────────────────────────────────────────────┤
│ Cloudflare Workers │ V8 isolates at the edge │
│ Vercel Edge │ V8 isolates, Next.js optimized │
│ Deno Deploy │ Deno at the edge │
│ Netlify Edge │ Deno-based edge functions │
│ AWS Lambda │ Node.js/custom runtimes │
│ Fastly Compute │ WebAssembly-based │
└─────────────────────────────────────────────────────────────────┘
PROBLEM: Each has different APIs!
// Node.js
const fs = require('fs');
const http = require('http');
// Deno
await Deno.readFile('file.txt');
Deno.serve(handler);
// Cloudflare Workers
export default { fetch(request, env, ctx) {} }
// Bun
Bun.serve({ fetch(req) {} });
Web Standards: The Universal Foundation
The solution is web standard APIs that all runtimes implement:
// WEB STANDARD APIs (work everywhere):
// Fetch API
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Request/Response
const request = new Request('https://example.com', {
method: 'POST',
body: JSON.stringify({ key: 'value' }),
});
const response = new Response('Hello', { status: 200 });
// URL
const url = new URL('https://example.com/path?query=value');
console.log(url.pathname); // '/path'
// Headers
const headers = new Headers();
headers.set('Content-Type', 'application/json');
// TextEncoder/TextDecoder
const encoder = new TextEncoder();
const bytes = encoder.encode('Hello');
// Crypto
const hash = await crypto.subtle.digest('SHA-256', data);
// Streams
const stream = new ReadableStream({ /* ... */ });
// AbortController
const controller = new AbortController();
fetch(url, { signal: controller.signal });
WinterCG (Web-interoperable Runtimes Community Group)
WinterCG standardizes APIs across server runtimes:
WINTERCG MEMBERS:
- Cloudflare
- Deno
- Node.js
- Vercel
- Shopify
- Bloomberg
- ... and more
STANDARDIZED APIs:
├── fetch(), Request, Response, Headers
├── URL, URLSearchParams, URLPattern
├── TextEncoder, TextDecoder
├── crypto.subtle (Web Crypto API)
├── Streams (ReadableStream, WritableStream)
├── AbortController, AbortSignal
├── setTimeout, setInterval
├── console
├── structuredClone
├── atob, btoa
└── Performance API
RESULT: Code using these APIs works on ANY compliant runtime
The UnJS Ecosystem
UnJS (Universal JavaScript) is a collection of packages for building universal JavaScript:
UNJS ECOSYSTEM:
┌─────────────────────────────────────────────────────────────────┐
│ FRAMEWORKS │
├─────────────────────────────────────────────────────────────────┤
│ Nitro │ Universal server builder (powers Nuxt) │
│ H3 │ Minimal HTTP framework │
│ Nuxt │ Full-stack Vue framework (uses Nitro) │
│ Analog │ Angular meta-framework (uses Nitro) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ CORE UTILITIES │
├─────────────────────────────────────────────────────────────────┤
│ ofetch │ Better fetch with auto-retry, interceptors │
│ unenv │ Runtime environment polyfills │
│ unbuild │ Unified build system for libraries │
│ unimport │ Auto-import utilities │
│ unstorage │ Universal storage layer │
│ uncrypto │ Universal crypto utilities │
│ unhead │ Universal document head manager │
│ unctx │ Composable async context │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ INFRASTRUCTURE │
├─────────────────────────────────────────────────────────────────┤
│ listhen │ Universal HTTP server listener │
│ ufo │ URL utilities │
│ pathe │ Cross-platform path utilities │
│ consola │ Universal logger │
│ defu │ Deep defaults merging │
│ hookable │ Async hooks system │
│ c12 │ Config loading utility │
└─────────────────────────────────────────────────────────────────┘
H3: The Universal HTTP Framework
H3 is a minimal, high-performance HTTP framework that runs everywhere:
// H3 BASICS:
import { createApp, createRouter, defineEventHandler } from 'h3';
const app = createApp();
const router = createRouter();
// Define routes
router.get('/hello', defineEventHandler((event) => {
return { message: 'Hello World' };
}));
router.post('/users', defineEventHandler(async (event) => {
const body = await readBody(event);
return { created: body };
}));
app.use(router);
// H3 is runtime-agnostic - it works with:
// - Node.js http server
// - Bun.serve
// - Deno.serve
// - Cloudflare Workers
// - Any web standard runtime
H3 Event Handler Model
// H3 uses an "event" abstraction over Request/Response:
import {
defineEventHandler,
getQuery,
getRouterParams,
readBody,
setCookie,
setHeader,
createError,
sendRedirect,
} from 'h3';
// Request utilities
const handler = defineEventHandler(async (event) => {
// URL query parameters
const query = getQuery(event); // ?page=1 → { page: '1' }
// Route parameters
const params = getRouterParams(event); // /users/:id → { id: '123' }
// Request body (auto-parsed)
const body = await readBody(event); // JSON, FormData, etc.
// Request headers
const auth = getHeader(event, 'authorization');
// Response utilities
setHeader(event, 'X-Custom', 'value');
setCookie(event, 'session', 'abc123');
// Return response (auto-serialized)
return { success: true }; // Becomes JSON response
});
// Error handling
const protectedHandler = defineEventHandler((event) => {
const user = getUser(event);
if (!user) {
throw createError({
statusCode: 401,
message: 'Unauthorized',
});
}
return { user };
});
// Redirects
const redirectHandler = defineEventHandler((event) => {
return sendRedirect(event, '/new-location', 302);
});
Why H3 Over Express/Fastify?
EXPRESS (Traditional Node.js):
┌─────────────────────────────────────────────────────────────────┐
│ - Built on Node.js http module │
│ - req/res are Node-specific objects │
│ - Middleware modifies req/res │
│ - Large ecosystem but Node-only │
│ - Callback-based (older pattern) │
│ │
│ LIMITATION: Only works on Node.js │
└─────────────────────────────────────────────────────────────────┘
H3 (Universal):
┌─────────────────────────────────────────────────────────────────┐
│ - Built on web standards (Request/Response) │
│ - Event abstraction over different runtimes │
│ - Composable with defineEventHandler │
│ - Tree-shakeable (small bundle) │
│ - TypeScript-first │
│ │
│ ADVANTAGE: Works everywhere via adapters │
└─────────────────────────────────────────────────────────────────┘
// Express (Node.js only):
app.get('/api', (req, res) => {
res.json({ message: 'Hello' });
});
// H3 (Universal):
router.get('/api', defineEventHandler(() => {
return { message: 'Hello' }; // Works on Node, Deno, Workers, etc.
}));
Nitro: Universal Server Builder
Nitro is the server framework that powers Nuxt. It builds universal server applications:
NITRO OVERVIEW:
┌─────────────────────────────────────────────────────────────────┐
│ YOUR CODE │
│ │
│ server/ │
│ ├── api/ API routes (auto-registered) │
│ │ ├── hello.ts → /api/hello │
│ │ └── users/ │
│ │ └── [id].ts → /api/users/:id │
│ ├── routes/ Custom routes │
│ ├── middleware/ Server middleware │
│ ├── plugins/ Server plugins │
│ └── utils/ Server utilities │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ NITRO BUILD │
│ │
│ 1. Scan routes and handlers │
│ 2. Bundle server code │
│ 3. Apply runtime-specific transforms │
│ 4. Generate optimized output │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────┴───────────────┐
│ PRESETS │
├───────────────────────────────┤
│ node-server │ Node.js │
│ cloudflare │ Workers │
│ vercel │ Vercel │
│ netlify │ Netlify │
│ deno │ Deno Deploy │
│ bun │ Bun │
│ aws-lambda │ Lambda │
│ ... 20+ more │ │
└───────────────────────────────┘
Nitro File-Based Routing
server/
├── api/
│ ├── hello.ts → GET /api/hello
│ ├── hello.post.ts → POST /api/hello
│ ├── users/
│ │ ├── index.ts → GET /api/users
│ │ ├── index.post.ts → POST /api/users
│ │ └── [id].ts → GET /api/users/:id
│ └── [...slug].ts → /api/* (catch-all)
├── routes/
│ └── feed.xml.ts → GET /feed.xml
└── middleware/
└── auth.ts → Runs on all requests
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id');
const user = await db.users.findById(id);
if (!user) {
throw createError({
statusCode: 404,
message: 'User not found',
});
}
return user;
});
// server/middleware/auth.ts
export default defineEventHandler((event) => {
const token = getHeader(event, 'authorization');
if (token) {
event.context.user = verifyToken(token);
}
});
Nitro Deployment Presets
// nitro.config.ts
export default defineNitroConfig({
// Target platform
preset: 'cloudflare-pages', // or 'vercel', 'netlify', 'node', etc.
// Preset-specific options
cloudflare: {
pages: {
routes: {
exclude: ['/static/*'],
},
},
},
});
// BUILD COMMAND:
// npx nitro build
// OUTPUT varies by preset:
// preset: 'node-server'
// → .output/
// ├── server/
// │ └── index.mjs (Node.js server)
// └── public/ (Static files)
// preset: 'cloudflare-pages'
// → .output/
// ├── _worker.js (Workers script)
// └── public/ (Static files)
// preset: 'vercel'
// → .vercel/
// └── output/
// ├── functions/ (Serverless functions)
// └── static/ (Static files)
How Nitro Achieves Universality
NITRO'S UNIVERSAL ARCHITECTURE:
┌─────────────────────────────────────────────────────────────────┐
│ YOUR H3 HANDLERS │
│ │
│ // Same code for all platforms │
│ defineEventHandler((event) => { │
│ return { message: 'Hello' }; │
│ }); │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ H3 LAYER │
│ │
│ Abstracts Request/Response handling │
│ Provides consistent event interface │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ UNENV LAYER │
│ │
│ Polyfills Node.js APIs for non-Node runtimes │
│ - process.env → runtime env vars │
│ - Buffer → Uint8Array │
│ - fs → unstorage │
│ - crypto → Web Crypto │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ RUNTIME ADAPTER │
│ │
│ Node.js: http.createServer(toNodeHandler(app)) │
│ Deno: Deno.serve(toWebHandler(app)) │
│ Workers: export default { fetch: toWebHandler(app) } │
│ Bun: Bun.serve({ fetch: toWebHandler(app) }) │
└─────────────────────────────────────────────────────────────────┘
unenv: The Environment Polyfill Layer
unenv provides Node.js API compatibility for non-Node runtimes:
// THE PROBLEM:
// Your code uses Node.js APIs:
import { readFileSync } from 'fs';
import { createHash } from 'crypto';
// But Cloudflare Workers doesn't have fs or crypto modules!
// ❌ Error: Cannot find module 'fs'
// UNENV SOLUTION:
// unenv provides mock/polyfill implementations:
// fs → Mock (errors or uses storage abstraction)
// crypto → Web Crypto API wrapper
// process → Minimal process-like object
// Buffer → Uint8Array wrapper
// In nitro.config.ts:
export default defineNitroConfig({
preset: 'cloudflare',
// unenv automatically applies polyfills
});
// Now your Node.js code "just works" on Workers
// (with some limitations for truly Node-specific APIs)
What unenv Polyfills
NODE.JS MODULE → UNENV REPLACEMENT
──────────────────────────────────────────────────────────────────
process → Minimal process object with env
Buffer → Uint8Array-based implementation
stream → Web Streams API
crypto → Web Crypto API
path → Pure JS implementation
url → Native URL class
util → Pure JS utilities
events → EventEmitter implementation
assert → Pure JS assert
NODE.JS MODULE → UNENV MOCK (no-op or error)
──────────────────────────────────────────────────────────────────
fs → Mock (use unstorage instead)
child_process → Mock (not available in edge)
net → Mock (not available in edge)
http → Use fetch instead
unstorage: Universal Storage
unstorage provides a unified storage API across different backends:
// UNSTORAGE BASICS:
import { createStorage } from 'unstorage';
import fsDriver from 'unstorage/drivers/fs';
import redisDriver from 'unstorage/drivers/redis';
import cloudflareKVDriver from 'unstorage/drivers/cloudflare-kv-binding';
// Development: File system
const devStorage = createStorage({
driver: fsDriver({ base: './data' }),
});
// Production (Redis):
const prodStorage = createStorage({
driver: redisDriver({ url: 'redis://localhost:6379' }),
});
// Cloudflare Workers:
const edgeStorage = createStorage({
driver: cloudflareKVDriver({ binding: 'MY_KV' }),
});
// SAME API everywhere:
await storage.setItem('user:123', { name: 'John' });
const user = await storage.getItem('user:123');
await storage.removeItem('user:123');
const keys = await storage.getKeys('user:');
Storage Drivers
DRIVER USE CASE
──────────────────────────────────────────────────────────────────
memory Local development, testing
fs Node.js file system
redis Production caching
cloudflare-kv-binding Cloudflare Workers KV
vercel-kv Vercel KV storage
netlify-blobs Netlify Blob storage
planetscale PlanetScale database
mongodb MongoDB collections
s3 AWS S3 buckets
github GitHub repository files
http Remote HTTP storage
lru-cache In-memory LRU cache
ofetch: Universal Fetch
ofetch is an enhanced fetch that works everywhere:
import { ofetch } from 'ofetch';
// BASIC USAGE (same as fetch):
const data = await ofetch('/api/users');
// AUTO-PARSING:
// JSON responses automatically parsed
const users = await ofetch('/api/users'); // Returns parsed JSON
// BASE URL:
const api = ofetch.create({ baseURL: 'https://api.example.com' });
const user = await api('/users/123');
// RETRY:
const data = await ofetch('/api/data', {
retry: 3,
retryDelay: 1000,
});
// INTERCEPTORS:
const api = ofetch.create({
onRequest({ options }) {
options.headers.set('Authorization', `Bearer ${token}`);
},
onResponse({ response }) {
console.log('Response:', response.status);
},
onResponseError({ response }) {
if (response.status === 401) {
logout();
}
},
});
// TYPE-SAFE:
interface User {
id: number;
name: string;
}
const user = await ofetch<User>('/api/users/123');
// user is typed as User
Comparing Server Approaches
┌────────────┬──────────────┬────────────────┬─────────────────┐
│ │ Express │ Fastify │ H3 │
├────────────┼──────────────┼────────────────┼─────────────────┤
│ Runtime │ Node.js only │ Node.js only │ Universal │
├────────────┼──────────────┼────────────────┼─────────────────┤
│ Standards │ Node http │ Node http │ Web standards │
├────────────┼──────────────┼────────────────┼─────────────────┤
│ Bundle │ ~200KB │ ~100KB │ ~20KB │
├────────────┼──────────────┼────────────────┼─────────────────┤
│ Edge ready │ ❌ │ ❌ │ ✅ │
├────────────┼──────────────┼────────────────┼─────────────────┤
│ TypeScript │ Manual types │ Built-in │ Built-in │
├────────────┼──────────────┼────────────────┼─────────────────┤
│ Ecosystem │ Huge │ Growing │ UnJS + adapters │
└────────────┴──────────────┴────────────────┴─────────────────┘
Deep Dive: Understanding Universal JavaScript
Why Runtime Diversity Exists
THE JAVASCRIPT RUNTIME HISTORY:
2009: Node.js
└─► JavaScript on servers (V8 engine)
└─► npm ecosystem explodes
└─► Everyone uses Node-specific APIs (fs, http, etc.)
2018: Cloudflare Workers
└─► JavaScript at the edge (V8 isolates)
└─► No file system, limited APIs
└─► Web standard APIs only
└─► Startup in milliseconds (vs seconds for Node)
2020: Deno
└─► "Fixed" Node.js design mistakes
└─► Web standards first
└─► Built-in TypeScript
└─► Secure by default (permissions)
2022: Bun
└─► Speed-focused runtime (JavaScriptCore engine)
└─► Node.js compatible
└─► Built-in bundler, test runner
└─► Faster npm, native APIs
2023+: More edge runtimes
└─► Vercel Edge Runtime
└─► Netlify Edge Functions
└─► AWS Lambda@Edge
└─► Fastly Compute
THE CHALLENGE:
- Code written for Node.js doesn't work on Workers
- Code written for Workers might not use Node.js ecosystem
- Each runtime has unique features/limitations
- Deployment target affects how you write code
THE SOLUTION: Write to web standards + abstract the differences
How V8 Isolates Enable Edge Computing
TRADITIONAL NODE.JS DEPLOYMENT:
┌─────────────────────────────────────────────────────────────────┐
│ EC2 Instance │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Node.js Process │ │
│ │ │ │
│ │ - Full V8 engine │ │
│ │ - Complete Node.js runtime │ │
│ │ - File system access │ │
│ │ - Network access │ │
│ │ - All npm packages available │ │
│ │ │ │
│ │ Startup: 500ms - 2s (cold start) │ │
│ │ Memory: 128MB - 1GB │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
CLOUDFLARE WORKERS (V8 Isolates):
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Edge Server │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Isolate 1 │ │ Isolate 2 │ │ Isolate 3 │ │ Isolate N │ │
│ │ (App A) │ │ (App B) │ │ (App A) │ │ (App X) │ │
│ │ │ │ │ │ │ │ │ │
│ │ ~128KB │ │ ~256KB │ │ ~128KB │ │ ~512KB │ │
│ │ 5ms cold │ │ 5ms cold │ │ 0ms warm │ │ 5ms cold │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ Single V8 engine, many isolated contexts │
│ No file system (security) │
│ Limited memory per isolate │
│ Millisecond cold starts │
└─────────────────────────────────────────────────────────────────┘
WHY ISOLATES ARE FAST:
1. V8 engine already warm (shared)
2. No process startup overhead
3. Minimal memory per request
4. Snapshot-based initialization
5. Thousands of isolates per machine
The Web Standard Request/Response Model
// THE WEB STANDARD MODEL:
// Everything is Request → Response
// INPUT: Request object
const request = new Request('https://example.com/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
},
body: JSON.stringify({ name: 'John' }),
});
// Properties available:
request.method // 'POST'
request.url // 'https://example.com/api/users'
request.headers // Headers object
request.body // ReadableStream
await request.json() // Parse body as JSON
await request.text() // Parse body as text
// OUTPUT: Response object
const response = new Response(
JSON.stringify({ id: 1, name: 'John' }),
{
status: 201,
headers: {
'Content-Type': 'application/json',
},
}
);
// A UNIVERSAL HANDLER:
async function handler(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/api/hello') {
return new Response('Hello World');
}
return new Response('Not Found', { status: 404 });
}
// This handler works on:
// - Cloudflare Workers: export default { fetch: handler }
// - Deno: Deno.serve(handler)
// - Bun: Bun.serve({ fetch: handler })
// - Node.js (with adapter): http.createServer(toNodeHandler(handler))
How H3 Abstracts Runtime Differences
// H3's event abstraction layer:
// H3 Event wraps different runtime request types:
interface H3Event {
node?: { // Node.js
req: IncomingMessage;
res: ServerResponse;
};
web?: { // Web standard
request: Request;
url: URL;
};
context: Record<string, any>; // Request context
}
// H3 utilities work regardless of underlying runtime:
export function getHeader(event: H3Event, name: string): string | undefined {
// Node.js path:
if (event.node) {
return event.node.req.headers[name.toLowerCase()];
}
// Web standard path:
if (event.web) {
return event.web.request.headers.get(name);
}
}
export function setHeader(event: H3Event, name: string, value: string): void {
// Node.js path:
if (event.node) {
event.node.res.setHeader(name, value);
}
// Web standard path:
if (event._responseHeaders) {
event._responseHeaders.set(name, value);
}
}
// YOUR CODE just uses the abstraction:
defineEventHandler((event) => {
const auth = getHeader(event, 'authorization');
setHeader(event, 'X-Custom', 'value');
return { message: 'Hello' };
});
// Works on Node, Deno, Workers, Bun without changes
Nitro's Build Process Deep Dive
NITRO BUILD PIPELINE:
INPUT:
server/
├── api/
│ └── users.ts // H3 handlers
├── routes/
│ └── index.ts
├── middleware/
│ └── auth.ts
└── plugins/
└── database.ts
STEP 1: SCAN
─────────────────────────────────────────────────────────────────
- Find all route files
- Parse route patterns from file names
- Discover middleware and plugins
- Build route table
Route Table:
[
{ path: '/api/users', handler: './api/users.ts', method: 'get' },
{ path: '/', handler: './routes/index.ts', method: 'get' },
]
STEP 2: BUNDLE WITH ROLLUP
─────────────────────────────────────────────────────────────────
- Bundle all handlers into single file
- Tree-shake unused code
- Apply preset-specific transforms
rollup.config:
input: virtual:nitro-entry
output: format based on preset (esm, cjs, iife)
STEP 3: APPLY UNENV TRANSFORMS
─────────────────────────────────────────────────────────────────
- Replace Node.js imports with polyfills
- Inject runtime-specific code
- Remove unavailable APIs
Example transforms:
import fs from 'fs' → import fs from 'unenv/runtime/node/fs'
import crypto from 'crypto' → import crypto from 'unenv/runtime/node/crypto'
STEP 4: GENERATE RUNTIME ENTRY
─────────────────────────────────────────────────────────────────
// Node.js preset:
import { createServer } from 'http';
import { toNodeHandler } from 'h3';
import { app } from './app.mjs';
createServer(toNodeHandler(app)).listen(3000);
// Cloudflare Workers preset:
import { toWebHandler } from 'h3';
import { app } from './app.mjs';
export default { fetch: toWebHandler(app) };
// Deno preset:
import { toWebHandler } from 'h3';
import { app } from './app.mjs';
Deno.serve(toWebHandler(app));
STEP 5: OUTPUT
─────────────────────────────────────────────────────────────────
.output/
├── server/
│ ├── index.mjs // Entry point
│ └── chunks/ // Code-split chunks
└── public/ // Static assets
Edge vs Serverless vs Traditional
┌─────────────────────────────────────────────────────────────────┐
│ TRADITIONAL SERVER │
├─────────────────────────────────────────────────────────────────┤
│ Location: Single region (us-east-1) │
│ Runtime: Node.js process (long-running) │
│ Cold start: 0ms (always warm) │
│ Scaling: Manual or auto-scaling groups │
│ Cost: Pay for uptime │
│ APIs: Full Node.js (fs, net, child_process) │
│ │
│ Best for: WebSocket, long computations, file processing │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ SERVERLESS │
├─────────────────────────────────────────────────────────────────┤
│ Location: Single or few regions │
│ Runtime: Node.js container (ephemeral) │
│ Cold start: 100ms - 3s │
│ Scaling: Automatic, per-request │
│ Cost: Pay per invocation │
│ APIs: Full Node.js (fs, net, etc.) │
│ │
│ Best for: Infrequent traffic, variable load, APIs │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ EDGE │
├─────────────────────────────────────────────────────────────────┤
│ Location: Global (200+ locations) │
│ Runtime: V8 isolate (lightweight) │
│ Cold start: < 10ms │
│ Scaling: Automatic, instant │
│ Cost: Pay per request (very cheap) │
│ APIs: Web standards only (limited) │
│ │
│ Best for: Low latency, personalization, A/B tests, auth │
└─────────────────────────────────────────────────────────────────┘
HYBRID APPROACH (Modern best practice):
┌─────────────────────────────────────────────────────────────────┐
│ Edge Layer: Auth, routing, personalization │
│ ↓ │
│ Serverless Layer: API routes, database queries │
│ ↓ │
│ Traditional Layer: WebSocket, heavy computation │
└─────────────────────────────────────────────────────────────────┘
Universal Storage Patterns
// THE STORAGE ABSTRACTION PATTERN:
// Different platforms have different storage:
// - Node.js: File system, Redis, PostgreSQL
// - Workers: KV, D1, R2
// - Vercel: Vercel KV, Postgres
// - AWS: S3, DynamoDB
// UNSTORAGE provides one API:
import { createStorage } from 'unstorage';
// Configuration varies by environment:
function createAppStorage() {
if (process.env.CLOUDFLARE) {
return createStorage({
driver: cloudflareKVBindingDriver({ binding: 'CACHE' }),
});
}
if (process.env.VERCEL) {
return createStorage({
driver: vercelKVDriver({ /* ... */ }),
});
}
// Development / Node.js
return createStorage({
driver: fsDriver({ base: './.data' }),
});
}
// YOUR CODE uses the same API everywhere:
const storage = createAppStorage();
export async function getUser(id: string) {
// Check cache first
const cached = await storage.getItem(`user:${id}`);
if (cached) return cached;
// Fetch from database
const user = await db.users.findById(id);
// Cache for 5 minutes
await storage.setItem(`user:${id}`, user, { ttl: 300 });
return user;
}
Practical: Building a Universal API
// A complete universal API example:
// server/api/posts/[id].ts
export default defineEventHandler(async (event) => {
// Works on any runtime
const id = getRouterParam(event, 'id');
const method = event.method;
// Universal storage
const storage = useStorage('cache');
if (method === 'GET') {
// Check cache
const cached = await storage.getItem(`post:${id}`);
if (cached) {
setHeader(event, 'X-Cache', 'HIT');
return cached;
}
// Fetch from database (works on edge via fetch)
const post = await ofetch(`https://api.example.com/posts/${id}`);
// Cache for 1 hour
await storage.setItem(`post:${id}`, post, { ttl: 3600 });
return post;
}
if (method === 'PUT') {
const body = await readBody(event);
// Update via API
const updated = await ofetch(`https://api.example.com/posts/${id}`, {
method: 'PUT',
body,
});
// Invalidate cache
await storage.removeItem(`post:${id}`);
return updated;
}
throw createError({ statusCode: 405, message: 'Method not allowed' });
});
// This single file works on:
// - Node.js server
// - Cloudflare Workers/Pages
// - Vercel Serverless/Edge
// - Netlify Functions/Edge
// - Deno Deploy
// - AWS Lambda
// - And more...
// Just change the Nitro preset in config!
When to Use What Runtime
DECISION MATRIX:
Use EDGE when:
├─► Request latency is critical (< 50ms)
├─► Doing auth, redirects, header manipulation
├─► A/B testing, personalization
├─► Simple data transformations
└─► Global user base
Use SERVERLESS when:
├─► Database connections needed
├─► Moderate computation
├─► Node.js ecosystem required
├─► Moderate traffic with spikes
└─► Cost optimization for variable load
Use TRADITIONAL when:
├─► WebSocket connections
├─► Long-running processes
├─► Heavy computation (ML, video)
├─► Predictable high traffic
└─► Full Node.js APIs needed
Use HYBRID when:
├─► Different parts have different needs
├─► Global reach with complex backend
├─► Optimizing for both latency and capability
└─► Most production applications!
For Framework Authors: Building Universal Runtime Systems
Implementation Note: The patterns and code examples below represent one proven approach to building universal runtime systems. The adapter pattern shown here is inspired by Nitro/H3 but other approaches exist—SvelteKit uses a similar adapter system, while Remix uses a different deployment abstraction. The direction shown here provides portable patterns based on web standards. Adapt based on which runtimes you need to support and your polyfill strategy.
Creating Runtime Adapters
// NITRO-STYLE ADAPTER PATTERN
class RuntimeAdapter {
constructor(name, options = {}) {
this.name = name;
this.options = options;
}
// Transform the universal handler for target runtime
async adapt(handler, buildOutput) {
throw new Error('Must implement adapt()');
}
// Generate entry point for runtime
generateEntry() {
throw new Error('Must implement generateEntry()');
}
// Get runtime-specific build config
getBuildConfig() {
return {};
}
}
// Node.js Adapter
class NodeAdapter extends RuntimeAdapter {
constructor(options = {}) {
super('node', options);
this.port = options.port || 3000;
}
generateEntry() {
return `
import { createServer } from 'node:http';
import { toNodeHandler } from 'h3';
import { app } from '#internal/app';
const handler = toNodeHandler(app);
const server = createServer(handler);
server.listen(${this.port}, () => {
console.log(\`Server running on http://localhost:${this.port}\`);
});
`.trim();
}
getBuildConfig() {
return {
target: 'node',
external: ['node:*'],
minify: false,
};
}
}
// Cloudflare Workers Adapter
class CloudflareAdapter extends RuntimeAdapter {
constructor(options = {}) {
super('cloudflare', options);
}
generateEntry() {
return `
import { toWebHandler } from 'h3';
import { app } from '#internal/app';
const handler = toWebHandler(app);
export default {
async fetch(request, env, ctx) {
// Inject environment bindings
globalThis.__env__ = env;
return handler(request, {
waitUntil: (p) => ctx.waitUntil(p),
passThroughOnException: () => ctx.passThroughOnException(),
});
},
};
`.trim();
}
getBuildConfig() {
return {
target: 'webworker',
format: 'esm',
external: [],
minify: true,
define: {
'process.env.NODE_ENV': '"production"',
},
};
}
}
// Vercel Edge Adapter
class VercelEdgeAdapter extends RuntimeAdapter {
constructor(options = {}) {
super('vercel-edge', options);
}
generateEntry() {
return `
import { toWebHandler } from 'h3';
import { app } from '#internal/app';
const handler = toWebHandler(app);
export const config = { runtime: 'edge' };
export default function (request) {
return handler(request);
}
`.trim();
}
// Generate vercel.json config
generateConfig(routes) {
return {
version: 3,
routes: routes.map(r => ({
src: r.pattern,
dest: r.handler,
})),
};
}
}
// AWS Lambda Adapter
class LambdaAdapter extends RuntimeAdapter {
constructor(options = {}) {
super('lambda', options);
}
generateEntry() {
return `
import { toWebHandler } from 'h3';
import { app } from '#internal/app';
const handler = toWebHandler(app);
export async function handler(event, context) {
// Convert Lambda event to Web Request
const request = lambdaToRequest(event);
// Handle request
const response = await handler(request);
// Convert Web Response to Lambda response
return responseToLambda(response);
}
function lambdaToRequest(event) {
const url = \`https://\${event.headers.host}\${event.rawPath}\`;
return new Request(url, {
method: event.requestContext.http.method,
headers: event.headers,
body: event.body,
});
}
async function responseToLambda(response) {
return {
statusCode: response.status,
headers: Object.fromEntries(response.headers),
body: await response.text(),
isBase64Encoded: false,
};
}
`.trim();
}
}
Implementing Polyfill Presets
// UNENV-STYLE POLYFILL SYSTEM
class EnvironmentPreset {
constructor(name) {
this.name = name;
this.polyfills = new Map();
this.aliases = new Map();
this.globals = new Map();
this.external = new Set();
}
// Register a polyfill for a module
polyfill(module, implementation) {
this.polyfills.set(module, implementation);
return this;
}
// Alias one module to another
alias(from, to) {
this.aliases.set(from, to);
return this;
}
// Define global injection
global(name, value) {
this.globals.set(name, value);
return this;
}
// Mark module as external
external(module) {
this.external.add(module);
return this;
}
// Generate bundler config
toBundlerConfig() {
return {
alias: Object.fromEntries(this.aliases),
define: Object.fromEntries(
[...this.globals].map(([k, v]) => [`globalThis.${k}`, v])
),
external: [...this.external],
};
}
}
// Node preset for edge runtimes
const nodePresetForEdge = new EnvironmentPreset('node-edge')
// Polyfill Node.js built-ins with web-compatible versions
.polyfill('node:buffer', 'unenv/runtime/node/buffer')
.polyfill('node:crypto', 'unenv/runtime/node/crypto')
.polyfill('node:stream', 'unenv/runtime/node/stream')
.polyfill('node:events', 'unenv/runtime/node/events')
.polyfill('node:util', 'unenv/runtime/node/util')
.polyfill('node:path', 'unenv/runtime/node/path')
.polyfill('node:url', 'unenv/runtime/node/url')
// Stub Node.js modules not available on edge
.polyfill('node:fs', 'unenv/runtime/mock/empty')
.polyfill('node:child_process', 'unenv/runtime/mock/empty')
.polyfill('node:net', 'unenv/runtime/mock/empty')
.polyfill('node:tls', 'unenv/runtime/mock/empty')
// Global injections
.global('process', 'unenv/runtime/node/process')
.global('Buffer', 'unenv/runtime/node/buffer')
// Aliases
.alias('buffer', 'node:buffer')
.alias('stream', 'node:stream')
.alias('events', 'node:events');
// Create polyfill bundle
function createPolyfillBundle(preset) {
const imports = [];
const exports = [];
for (const [module, impl] of preset.polyfills) {
const safeName = module.replace(/[^a-z0-9]/gi, '_');
imports.push(`import * as ${safeName} from '${impl}';`);
exports.push(`'${module}': ${safeName},`);
}
return `
${imports.join('\n')}
export const polyfills = {
${exports.join('\n ')}
};
// Auto-inject polyfills
for (const [name, impl] of Object.entries(polyfills)) {
globalThis.__modules__ = globalThis.__modules__ || {};
globalThis.__modules__[name] = impl;
}
`.trim();
}
Building a Universal Storage Adapter
// UNSTORAGE-STYLE DRIVER SYSTEM
class StorageDriver {
constructor(options = {}) {
this.options = options;
}
// Required methods
async hasItem(key) { throw new Error('Not implemented'); }
async getItem(key) { throw new Error('Not implemented'); }
async setItem(key, value) { throw new Error('Not implemented'); }
async removeItem(key) { throw new Error('Not implemented'); }
async getKeys(base) { throw new Error('Not implemented'); }
async clear() { throw new Error('Not implemented'); }
// Optional methods
async getMeta(key) { return {}; }
async setMeta(key, meta) {}
}
// Memory driver (works everywhere)
class MemoryDriver extends StorageDriver {
constructor(options = {}) {
super(options);
this.data = new Map();
this.meta = new Map();
}
async hasItem(key) {
return this.data.has(key);
}
async getItem(key) {
return this.data.get(key) ?? null;
}
async setItem(key, value) {
this.data.set(key, value);
this.meta.set(key, { mtime: Date.now() });
}
async removeItem(key) {
this.data.delete(key);
this.meta.delete(key);
}
async getKeys(base = '') {
return [...this.data.keys()].filter(k => k.startsWith(base));
}
async clear() {
this.data.clear();
this.meta.clear();
}
}
// Cloudflare KV driver
class CloudflareKVDriver extends StorageDriver {
constructor(options) {
super(options);
this.binding = options.binding; // KV namespace binding name
}
getKV() {
// Access from Cloudflare env
return globalThis.__env__?.[this.binding];
}
async hasItem(key) {
return (await this.getKV().get(key)) !== null;
}
async getItem(key) {
const value = await this.getKV().get(key, 'text');
return value ? JSON.parse(value) : null;
}
async setItem(key, value) {
await this.getKV().put(key, JSON.stringify(value), {
expirationTtl: this.options.ttl,
});
}
async removeItem(key) {
await this.getKV().delete(key);
}
async getKeys(base = '') {
const list = await this.getKV().list({ prefix: base });
return list.keys.map(k => k.name);
}
}
// Universal storage factory
function createStorage(options = {}) {
const drivers = new Map();
return {
// Mount driver at prefix
mount(prefix, driver) {
drivers.set(prefix, driver);
},
// Get driver for key
getDriver(key) {
for (const [prefix, driver] of drivers) {
if (key.startsWith(prefix)) {
return { driver, key: key.slice(prefix.length) };
}
}
return { driver: drivers.get('') || new MemoryDriver(), key };
},
// Unified interface
async getItem(key) {
const { driver, key: k } = this.getDriver(key);
return driver.getItem(k);
},
async setItem(key, value, opts) {
const { driver, key: k } = this.getDriver(key);
return driver.setItem(k, value, opts);
},
async removeItem(key) {
const { driver, key: k } = this.getDriver(key);
return driver.removeItem(k);
},
};
}
Implementing Cross-Runtime Crypto
// WEB CRYPTO ABSTRACTION (UNCRYPTO-STYLE)
// Universal crypto that works on Node, Deno, Bun, Edge
const crypto = globalThis.crypto || (await import('node:crypto')).webcrypto;
class UniversalCrypto {
// Random bytes
static getRandomBytes(length) {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return bytes;
}
// Generate UUID
static randomUUID() {
return crypto.randomUUID();
}
// Hash with SHA-256
static async sha256(data) {
const encoder = new TextEncoder();
const buffer = typeof data === 'string' ? encoder.encode(data) : data;
const hash = await crypto.subtle.digest('SHA-256', buffer);
return new Uint8Array(hash);
}
// Hash to hex string
static async sha256Hex(data) {
const hash = await this.sha256(data);
return Array.from(hash)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// HMAC signing
static async hmacSign(key, data) {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(key),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign(
'HMAC',
keyMaterial,
encoder.encode(data)
);
return new Uint8Array(signature);
}
// AES-GCM encryption
static async encrypt(key, data) {
const encoder = new TextEncoder();
const iv = this.getRandomBytes(12);
const keyMaterial = await crypto.subtle.importKey(
'raw',
await this.sha256(key),
'AES-GCM',
false,
['encrypt']
);
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
keyMaterial,
encoder.encode(data)
);
// Combine IV + encrypted data
const result = new Uint8Array(iv.length + encrypted.byteLength);
result.set(iv);
result.set(new Uint8Array(encrypted), iv.length);
return result;
}
// AES-GCM decryption
static async decrypt(key, encryptedData) {
const iv = encryptedData.slice(0, 12);
const data = encryptedData.slice(12);
const keyMaterial = await crypto.subtle.importKey(
'raw',
await this.sha256(key),
'AES-GCM',
false,
['decrypt']
);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
keyMaterial,
data
);
return new TextDecoder().decode(decrypted);
}
}
// Base64 URL encoding (works everywhere)
function base64UrlEncode(data) {
const bytes = typeof data === 'string'
? new TextEncoder().encode(data)
: data;
const base64 = btoa(String.fromCharCode(...bytes));
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
function base64UrlDecode(str) {
const base64 = str
.replace(/-/g, '+')
.replace(/_/g, '/');
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
Building Universal Fetch Wrapper
// OFETCH-STYLE UNIVERSAL FETCH
function createFetch(defaults = {}) {
return async function $fetch(url, options = {}) {
const config = {
...defaults,
...options,
headers: {
...defaults.headers,
...options.headers,
},
};
// Resolve URL
const resolvedUrl = config.baseURL
? new URL(url, config.baseURL).toString()
: url;
// Prepare request
const fetchOptions = {
method: config.method || 'GET',
headers: config.headers,
signal: config.signal,
};
// Handle body
if (config.body !== undefined) {
if (typeof config.body === 'object' &&
!(config.body instanceof FormData) &&
!(config.body instanceof ReadableStream)) {
fetchOptions.body = JSON.stringify(config.body);
fetchOptions.headers['Content-Type'] = 'application/json';
} else {
fetchOptions.body = config.body;
}
}
// Add query params
if (config.query) {
const separator = resolvedUrl.includes('?') ? '&' : '?';
const params = new URLSearchParams(config.query).toString();
url = resolvedUrl + separator + params;
}
// Retry logic
const maxRetries = config.retry ?? 1;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// Interceptors - request
if (config.onRequest) {
await config.onRequest({ request: fetchOptions, options: config });
}
const response = await fetch(resolvedUrl, fetchOptions);
// Interceptors - response
if (config.onResponse) {
await config.onResponse({ response, options: config });
}
// Handle errors
if (!response.ok) {
const error = new FetchError(response.statusText);
error.response = response;
error.status = response.status;
if (config.onResponseError) {
await config.onResponseError({ error, response, options: config });
}
throw error;
}
// Parse response
const contentType = response.headers.get('content-type') || '';
if (config.responseType === 'stream') {
return response.body;
}
if (config.responseType === 'blob') {
return response.blob();
}
if (config.responseType === 'arrayBuffer') {
return response.arrayBuffer();
}
if (contentType.includes('application/json')) {
return response.json();
}
return response.text();
} catch (error) {
lastError = error;
// Don't retry on certain errors
if (error.status && error.status < 500) {
throw error;
}
// Wait before retry
if (attempt < maxRetries) {
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
}
}
}
throw lastError;
};
}
class FetchError extends Error {
constructor(message) {
super(message);
this.name = 'FetchError';
}
}
// Create default instance
const $fetch = createFetch();
// Create with defaults
const api = createFetch({
baseURL: 'https://api.example.com',
headers: {
'Authorization': 'Bearer token',
},
retry: 2,
});
Related Skills
- See meta-frameworks-overview for framework deployment options
- See rendering-patterns for SSR on edge
- See build-pipelines-bundling for build output formats