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, implementati
…(truncated)