| name | universal-javascript-runtimes |
| description | Explains universal JavaScript deployment, the UnJS ecosystem (Nitro, H3, unenv), WinterCG web standards, and cross-platform runtimes. Use when building apps that deploy anywhere, understanding server adapters, or working with edge functions and serverless platforms. |
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:
const response = await fetch('https://api.example.com/data');
const data = await response.json();
const request = new Request('https://example.com', {
method: 'POST',
body: JSON.stringify({ key: 'value' }),
});
const response = new Response('Hello', { status: 200 });
const url = new URL('https://example.com/path?query=value');
console.log(url.pathname);
const headers = new Headers();
headers.set('Content-Type', 'application/json');
const encoder = new TextEncoder();
const bytes = encoder.encode('Hello');
const hash = await crypto.subtle.digest('SHA-256', data);
const stream = new ReadableStream({ });
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:
import { createApp, createRouter, defineEventHandler } from 'h3';
const app = createApp();
const router = createRouter();
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 Event Handler Model
import {
defineEventHandler,
getQuery,
getRouterParams,
readBody,
setCookie,
setHeader,
createError,
sendRedirect,
} from 'h3';
const handler = defineEventHandler(async (event) => {
const query = getQuery(event);
const params = getRouterParams(event);
const body = await readBody(event);
const auth = getHeader(event, 'authorization');
setHeader(event, 'X-Custom', 'value');
setCookie(event, 'session', 'abc123');
return { success: true };
});
const protectedHandler = defineEventHandler((event) => {
const user = getUser(event);
if (!user) {
throw createError({
statusCode: 401,
message: 'Unauthorized',
});
}
return { user };
});
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
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;
});
export default defineEventHandler((event) => {
const token = getHeader(event, 'authorization');
if (token) {
event.context.user = verifyToken(token);
}
});
Nitro Deployment Presets
export default defineNitroConfig({
preset: 'cloudflare-pages',
cloudflare: {
pages: {
routes: {
exclude: ['/static/*'],
},
},
},
});
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:
import { readFileSync } from 'fs';
import { createHash } from 'crypto';
export default defineNitroConfig({
preset: 'cloudflare',
});
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:
import { createStorage } from 'unstorage';
import fsDriver from 'unstorage/drivers/fs';
import redisDriver from 'unstorage/drivers/redis';
import cloudflareKVDriver from 'unstorage/drivers/cloudflare-kv-binding';
const devStorage = createStorage({
driver: fsDriver({ base: './data' }),
});
const prodStorage = createStorage({
driver: redisDriver({ url: 'redis://localhost:6379' }),
});
const edgeStorage = createStorage({
driver: cloudflareKVDriver({ binding: 'MY_KV' }),
});
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';
const data = await ofetch('/api/users');
const users = await ofetch('/api/users');
const api = ofetch.create({ baseURL: 'https://api.example.com' });
const user = await api('/users/123');
const data = await ofetch('/api/data', {
retry: 3,
retryDelay: 1000,
});
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();
}
},
});
interface User {
id: number;
name: string;
}
const user = await ofetch<User>('/api/users/123');
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
const request = new Request('https://example.com/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
},
body: JSON.stringify({ name: 'John' }),
});
request.method
request.url
request.headers
request.body
await request.json()
await request.text()
const response = new Response(
JSON.stringify({ id: 1, name: 'John' }),
{
status: 201,
headers: {
'Content-Type': 'application/json',
},
}
);
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 });
}
How H3 Abstracts Runtime Differences
interface H3Event {
node?: {
req: IncomingMessage;
res: ServerResponse;
};
web?: {
request: Request;
url: URL;
};
context: Record<string, any>;
}
export function getHeader(event: H3Event, name: string): string | undefined {
if (event.node) {
return event.node.req.headers[name.toLowerCase()];
}
if (event.web) {
return event.web.request.headers.get(name);
}
}
export function setHeader(event: H3Event, name: string, value: string): void {
if (event.node) {
event.node.res.setHeader(name, value);
}
if (event._responseHeaders) {
event._responseHeaders.set(name, value);
}
}
defineEventHandler((event) => {
const auth = getHeader(event, 'authorization');
setHeader(event, 'X-Custom', 'value');
return { message: 'Hello' };
});
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
import { createStorage } from 'unstorage';
function createAppStorage() {
if (process.env.CLOUDFLARE) {
return createStorage({
driver: cloudflareKVBindingDriver({ binding: 'CACHE' }),
});
}
if (process.env.VERCEL) {
return createStorage({
driver: vercelKVDriver({ }),
});
}
return createStorage({
driver: fsDriver({ base: './.data' }),
});
}
const storage = createAppStorage();
export async function getUser(id: string) {
const cached = await storage.getItem(`user:${id}`);
if (cached) return cached;
const user = await db.users.findById(id);
await storage.setItem(`user:${id}`, user, { ttl: 300 });
return user;
}
Practical: Building a Universal API
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id');
const method = event.method;
const storage = useStorage('cache');
if (method === 'GET') {
const cached = await storage.getItem(`post:${id}`);
if (cached) {
setHeader(event, 'X-Cache', 'HIT');
return cached;
}
const post = await ofetch(`https://api.example.com/posts/${id}`);
await storage.setItem(`post:${id}`, post, { ttl: 3600 });
return post;
}
if (method === 'PUT') {
const body = await readBody(event);
const updated = await ofetch(`https://api.example.com/posts/${id}`, {
method: 'PUT',
body,
});
await storage.removeItem(`post:${id}`);
return updated;
}
throw createError({ statusCode: 405, message: 'Method not allowed' });
});
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
class RuntimeAdapter {
constructor(name, options = {}) {
this.name = name;
this.options = options;
}
async adapt(handler, buildOutput) {
throw new Error('Must implement adapt()');
}
generateEntry() {
throw new Error('Must implement generateEntry()');
}
getBuildConfig() {
return {};
}
}
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,
};
}
}
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"',
},
};
}
}
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();
}
generateConfig(routes) {
return {
version: 3,
routes: routes.map(r => ({
src: r.pattern,
dest: r.handler,
})),
};
}
}
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
class EnvironmentPreset {
constructor(name) {
this.name = name;
this.polyfills = new Map();
this.aliases = new Map();
this.globals = new Map();
this.external = new Set();
}
polyfill(module, implementation) {
this.polyfills.set(module, implementation);
return this;
}
alias(from, to) {
this.aliases.set(from, to);
return this;
}
global(name, value) {
this.globals.set(name, value);
return this;
}
external(module) {
this.external.add(module);
return this;
}
toBundlerConfig() {
return {
alias: Object.fromEntries(this.aliases),
define: Object.fromEntries(
[...this.globals].map(([k, v]) => [`globalThis.${k}`, v])
),
external: [...this.external],
};
}
}
const nodePresetForEdge = new EnvironmentPreset('node-edge')
.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')
.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('process', 'unenv/runtime/node/process')
.global('Buffer', 'unenv/runtime/node/buffer')
.alias('buffer', 'node:buffer')
.alias('stream', 'node:stream')
.alias('events', 'node:events');
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
class StorageDriver {
constructor(options = {}) {
this.options = options;
}
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'); }
async getMeta(key) { return {}; }
async setMeta(key, meta) {}
}
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();
}
}
class CloudflareKVDriver extends StorageDriver {
constructor(options) {
super(options);
this.binding = options.binding;
}
getKV() {
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);
}
}
function createStorage(options = {}) {
const drivers = new Map();
return {
mount(prefix, driver) {
drivers.set(prefix, driver);
},
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 };
},
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
const crypto = globalThis.crypto || (await import('node:crypto')).webcrypto;
class UniversalCrypto {
static getRandomBytes(length) {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return bytes;
}
static randomUUID() {
return crypto.randomUUID();
}
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);
}
static async sha256Hex(data) {
const hash = await this.sha256(data);
return Array.from(hash)
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
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);
}
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)
);
const result = new Uint8Array(iv.length + encrypted.byteLength);
result.set(iv);
result.set(new Uint8Array(encrypted), iv.length);
return result;
}
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);
}
}
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
function createFetch(defaults = {}) {
return async function $fetch(url, options = {}) {
const config = {
...defaults,
...options,
headers: {
...defaults.headers,
...options.headers,
},
};
const resolvedUrl = config.baseURL
? new URL(url, config.baseURL).toString()
: url;
const fetchOptions = {
method: config.method || 'GET',
headers: config.headers,
signal: config.signal,
};
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;
}
}
if (config.query) {
const separator = resolvedUrl.includes('?') ? '&' : '?';
const params = new URLSearchParams(config.query).toString();
url = resolvedUrl + separator + params;
}
const maxRetries = config.retry ?? 1;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
if (config.onRequest) {
await config.onRequest({ request: fetchOptions, options: config });
}
const response = await fetch(resolvedUrl, fetchOptions);
if (config.onResponse) {
await config.onResponse({ response, options: config });
}
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;
}
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;
if (error.status && error.status < 500) {
throw error;
}
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';
}
}
const $fetch = createFetch();
const api = createFetch({
baseURL: 'https://api.example.com',
headers: {
'Authorization': 'Bearer token',
},
retry: 2,
});
Related Skills