How Remix achieves true runtime portability across Node.js, Bun, Deno, and Cloudflare Workers
Remix packages work seamlessly across all JavaScript runtimes without modification. Write your code once and run it everywhere - Node.js, Bun, Deno, Cloudflare Workers, and any future runtime that implements web standards.
import { parseMultipartRequest } from 'remix/multipart-parser'async function handleUpload(request: Request): Promise<Response> { let files: Array<{ name: string; size: number }> = [] // This code works everywhere - no runtime-specific APIs for await (let part of parseMultipartRequest(request)) { if (part.isFile) { files.push({ name: part.filename, size: part.size, }) // Save file (implementation varies by runtime) await saveFile(part.filename, part.bytes) } } return Response.json({ files })}
// Works with: Bun, Deno, Cloudflare Workers, Node.js (with undici)import { parseMultipartRequest } from 'remix/multipart-parser'// Expects: Request with ReadableStream bodyfor await (let part of parseMultipartRequest(request)) { // ...}
// Works with: Node.js http.IncomingMessageimport { parseMultipartRequest } from 'remix/multipart-parser/node'// Expects: http.IncomingMessagehttp.createServer(async (req, res) => { for await (let part of parseMultipartRequest(req)) { // ... }})
The Node-specific entry point exists because Node’s http.IncomingMessage uses Node.js streams instead of Web Streams. The parsing logic is identical - only the input adapter differs.
The fetch-router package is completely runtime agnostic:
import { createRouter } from 'remix/fetch-router'import { route } from 'remix/fetch-router/routes'// This router works everywherelet routes = route({ api: { users: '/api/users', posts: '/api/posts', },})let router = createRouter()router.map(routes.api, { actions: { users: () => Response.json({ users: [] }), posts: () => Response.json({ posts: [] }), },})// Test with standard fetchlet response = await router.fetch('https://api.example.com/api/users')
Connect to any runtime:
Node.js
Bun
Deno
Cloudflare Workers
import * as http from 'node:http'import { createRequestListener } from 'remix/node-fetch-server'let server = http.createServer(createRequestListener(router.fetch))server.listen(3000)
import { openLazyFile } from 'remix/fs'// This code works in any runtimelet file = openLazyFile('./video.mp4')return new Response(file.stream(), { headers: { 'Content-Type': 'video/mp4', 'Content-Length': String(file.size), 'Accept-Ranges': 'bytes', },})
Node.js
Bun
Cloudflare Workers
import * as http from 'node:http'import { createRequestListener } from 'remix/node-fetch-server'import { openLazyFile } from 'remix/fs'async function handler(request: Request) { let file = openLazyFile('./video.mp4') return new Response(file.stream(), { headers: { 'Content-Type': 'video/mp4', 'Content-Length': String(file.size), }, })}http.createServer(createRequestListener(handler)).listen(3000)
import { openLazyFile } from 'remix/fs'Bun.serve({ async fetch(request) { let file = openLazyFile('./video.mp4') return new Response(file.stream(), { headers: { 'Content-Type': 'video/mp4', 'Content-Length': String(file.size), }, }) },})
export default { async fetch(request, env) { // Stream from R2 instead of filesystem let object = await env.BUCKET.get('video.mp4') return new Response(object.body, { headers: { 'Content-Type': 'video/mp4', 'Content-Length': String(object.size), }, }) },}