Engineering analysis · Bun vs Node

Bun APIs, mapped to Node

An API-level breakdown of every Bun primitive used by the reepolee framework and the reeweb static-site generator - with the equivalent Node API or npm package for each one. Every row links to the Bun API reference.

Two projects, one runtime

reepolee

Full-stack application framework: HTTP server, SQL data layer, S3 storage, Redis cache and queue, websockets, a code generator, and CLI scripts. Runs 100% on Bun - no deps, no ceremony, full control.

  • Bun.serve() with native Radix-tree routes: in production
  • import { SQL } from "bun" - one client for SQLite, MySQL, Postgres
  • S3Client, RedisClient, Cookie as named bun imports
  • git clone, bun start. Done.
reeweb

Static-site generator with a dev server, preview/publisher daemons, and an image pipeline. Deploys to Cloudflare Workers (wrangler), while all build and CLI tooling runs on Bun.

  • SSG + dev server built on Bun.serve(), Bun.file(), Bun.write()
  • Bun.Image for responsive WebP/AVIF/JPEG generation
  • Bun.markdown.html() and Bun.YAML.parse() for content
  • cf-worker.ts uses Cloudflare's HTMLRewriter, not Bun
0
npm runtime dependencies
16
API categories mapped
58
Bun API functions mapped
2
projects analyzed

What this analysis covers

Both projects use Bun-native primitives end-to-end: file I/O, process spawning, hashing, markdown, YAML, archives, images, and the full data layer (SQL, S3, Redis). Below is the complete map - Bun API, where it is used, and what it would become on Node.

1. Core runtime globals

~250 usages across both projects (apps/main/server.ts, lib/bootstrap.ts, all scripts) - typed env access, auto-loads .env, settable at runtime
process.env; built-in .env loading via process.loadEnvFile() (v20.12+) / --env-file flag (v20.6+)
Process arguments incl. script path (--dev / --prod / --agent / --test in apps/main/server.ts and scripts/*.ts)
process.argv.slice(2), or node:util parseArgs() (stable v20+)
Entry-file path checks (scripts/check_domain_compliance.ts)
require.main === module (CJS) or compare process.argv[1]
Locate executables in PATH (reeweb/scripts/test_replay.ts)
which npm package, or child_process.execSync("which …")
Async sleep in smoke tests and dev scripts
node:timers/promisessetTimeout(ms)
ANSI terminal colors in generator output
node:util styleText() (stable v22.11+), or chalk / picocolors
Async stdin iteration (MCP JSON-RPC server)
process.stdin async iteration
hrtime.bigint(), exit, cwd(), pid, platform, kill, signal handlers - run under Bun's Node-compat layer
Native Node - no change needed

2. HTTP server & WebSockets

The biggest rewrite surface: Bun.serve is fetch-handler based, not request/response-object based.
lib/server_startup.ts, apps/reeman/server.ts, dev/preview/publisher scripts, tests
node:http.createServer() or Express/Fastify/Hono - not drop-in
Production Radix-tree routing in apps/main/server.ts / apps/reeman/server.ts
No built-in; router frameworks (Express, Hono, Fastify)
.port, .upgrade(), .reload() in lib/bootstrap.ts, apps/main/server.ts
node:http.Server (no upgrade helper - see ws)
~40 imports in apps/reeman/**, lib/crud_routes.ts, lib/middleware/*
node:http.IncomingMessage + manual URL/body parsing
WS open/message/close config in lib/bootstrap.ts, apps/main/server.ts
ws npm package (WebSocketServer)
WS upgrade from fetch handler (/__reload livereload)
ws + http upgrade event (wss.handleUpgrade)
Dev live-editing WS in reeweb/scripts/dev/*.ts
Global WebSocket client built in (default v21+, stable v22.4+); server side still needs ws
Request / Response / fetch()
Web-standard HTTP primitives in every handler and test
Same web standards (Node 18+ has global fetch/Response)
Stream a file as the response body (scripts/preview.ts, dev responses)
fs.createReadStream() piped to the response

3. File system

~150 usages - lazy file handle
node:fs/promises.open() / fs.readFile
Read as string (init scripts, template engine, install)
fs.promises.readFile(path, "utf8")
Read and parse JSON (package.json, translations)
JSON.parse(await fs.promises.readFile(path, "utf8"))
Read as bytes (pack-addon tests, asset sync)
fs.promises.readFile(path)Buffer
Read as ArrayBuffer (image pipeline)
fs.promises.readFilebuf.buffer
Existence checks (bundle cache, generators)
fs.promises.access(path) or fs.existsSync
File metadata (env sync scripts)
fs.promises.stat(path)
Delete a file (lib/bundle_cache.ts)
fs.promises.unlink(path)
~100 usages - write files and directories
fs.promises.writeFile + mkdir(…, { recursive: true })
Glob matching (json_to_sql.ts, publisher/files.ts, og-images)
node:fs glob() / globSync() (stable v22.12+), or fast-glob / glob

4. Child processes & shell

Async subprocesses (dev orchestrate, publisher/runner, clone_db)
node:child_process.spawn() - different arg shape: Bun takes { cmd: ["bin", "arg"] }
Sync subprocesses (typecheck_ratchet, release, git_ship)
node:child_process.spawnSync()
.exited, .stdout, .kill() in publisher runner and orchestrate
node:child_process.ChildProcess + promisified exit
Shell template tag (Bun.$`git add -A) in git scripts
execa / zx npm, or child_process.execFile
scripts/mcp/operations.ts, generator/crud/refresh_fields.ts
require("node:child_process").spawnSync

5. Bundler & transpiler

Server-side JS bundling (lib/bundle_cache.ts)
esbuild / rollup / webpack npm
TS→JS transpile (reeweb/scripts/engine_drift_check.ts)
typescript ts.transpileModule() or esbuild/swc; Node 22.6+/24+ also runs .ts natively (type stripping)

6. Crypto & password hashing

sha1/sha256 (release_files, localized_hash, dynamic_assets)
node:crypto createHash().update().digest(), or one-shot crypto.hash() (v21.7+/22+)
hash() / verify() bcrypt for auth routes and user generator
bcrypt npm (hash/compare), or node:crypto.scrypt

7. Data formats (markdown / YAML)

Markdown → HTML in template helpers, the .ree compiler, SSG
marked / markdown-it / remark npm (no built-in)
YAML frontmatter (reeweb/lib/static_site.ts)
yaml / js-yaml npm

8. Database - bun:sql (import { SQL } from "bun")

Bun's SQL client speaks SQLite, MySQL and Postgres from a single import.
SQL
config/db.ts, config/db_cli.ts, lib/i18n.ts, generators, MCP db tool
SQLite is built in: node:sqlite (v22.5+ behind a flag, unflagged v22.12+, stable v24+; ships in Node 26). MySQL → mysql2, Postgres → pg; better-sqlite3 as a SQLite alternative
db_cli`SELECT …` built-in query interpolation
sql-template-strings, pg tagged templates, or Knex/Prisma

9. Object storage - bun:s3

lib/s3/core.ts, lib/issue_reporter.ts (Cloudflare R2)
@aws-sdk/client-s3 npm (R2/S3-compatible)
Lazy file handles: .write(), .presign(), .exists()
@aws-sdk/lib-storage + Upload/GetObjectCommand

10. Redis - bun:redis

Queue worker, session store, rate-limit store
ioredis / redis npm packages
Singleton for cache and feature flags
ioredis default client instance

11. Cookies

Parse/serialize cookie headers (lib/cookies.ts, auth routes)
cookie npm package (parse/serialize)

12. Archive & images

tar create/read (pack_addon.ts, install_archive.ts)
tar npm package (no built-in)
Native image processing (lib/s3/proxy.ts, prepare_images.ts)
sharp npm package (or jimp); vips CLI

13. import.meta - module metadata

~200 usages - dirname of the current file
path.dirname(fileURLToPath(import.meta.url))
Absolute file path (entry checks)
fileURLToPath(import.meta.url)
Entry-point checks in scripts (ssg.ts, release.ts)
process.argv[1] === fileURLToPath(import.meta.url)
Module specifier resolution (apps/main/routes.ts)
import.meta.resolve() (Node 20.6+) or createRequire(...).resolve()

14. Test runner - bun:test

describe / expect / test / mock in ~90 test files
node:test + node:assert (Node 18+), or vitest/jest
Test runner in package.json scripts
node --test (concurrency via --test-concurrency); coverage via --test-coverage (stable v20.12+)
[test] / [run] config in both projects
.npmrc / package.json config fields
Function mocking in server and lib tests
node:test mock.fn() / mock.method() (stable v20+), or vi.fn() / jest.fn()

15. CLI / runtime flags

Package.json script runner in both projects
npm run
Run npm binaries (bunx tsc in typecheck_ratchet)
npx
In-place hot reload without process restart (apps/main/server.ts dev path)
node --watch (Node 18.11+; restarts the process)
Lockfile in both projects
package-lock.json / yarn.lock / pnpm-lock.yaml
tw via Bun-managed vendor
Tailwind CLI fetched and run by Bun scripts (css:build)
tailwindcss npm CLI

16. Cloudflare Workers - reeweb production runtime

reeweb is the only part that does not run on Bun in production: wrangler deploy uploads cf-worker.ts to Cloudflare's edge runtime (V8 isolates + web-standard fetch).
fetch(req, env) + env.ASSETS.fetch()
cf-worker.ts static asset handler
http.createServer + static file serving
HTMLRewriter
Streaming HTML transform (injects edge colo into [data-cf-edge])
cheerio / htmlrewriter npm, or regex replace
req.cf
Cloudflare request metadata in cf-worker.ts
Not available in Node - drop or use CDN headers

17. The .ree template syntax

The template language both projects use (apps/main/*.ree, src/public/*.ree), compiled by lib/template/compiler.ts.
{#layout("layout")}
Wrap the page in a layout template (layout.ree, docs.layout.ree, ...)
{#layout("layout")} at the top of every page
{#include('path', { data }) }
Inline a partial or component with data
{#include('$components/svgs/github.svg')} with $components / $lib / $config / $root path aliases
{{ ... }}
Raw JavaScript block, runs once at render
{{ const t = props.translations; props.page_title = t.title; }}
{= expr }
Escaped (HTML-escaped) output expression
{= project.title}
{~ expr }
Unescaped output expression
{~ helpers.md(project.desc)}; layouts emit the page via {~ props.body}
{_ path }
Escaped translation lookup on props.translations
{_ nav.theme_toggle}
{- path }
Unescaped translation lookup; inside a custom element it fills the children slot
{- intro_p}, <md-text>{- features_h}</md-text>
{@ path }
Translation lookup rendered as Markdown
{@ md_content }
{#each list }
Loop over an array or object (as item, index, key); {:else} renders the empty state
{#each props.translations.sections as section} ... {/each}
{#if cond }
Conditional rendering; {:else} for the fallback branch
{#if section.note} ... {/if}
{#with expr }
Scope block (JS with semantics)
{#with props.row} ... {/with}
<component>
Custom elements resolved from src/components/*.ree
<page-hero ...hero></page-hero>, <md-text type="h2" as="h2">, <link-button href="/docs">
...obj spread
Pass a props object into a custom element
<page-hero ...hero>
Locale variants
name.en-us.ree / name.sl-si.ree resolve per locale, falling back to name.ree
about.sl-si.ree
Built-in helpers
Bare identifiers in every template: localized_path(), url(), md(), nav_label(), is_current(), display_currency(), ...
Custom helpers via helpers.* (e.g. helpers.md_inline(), helpers.highlight())

Key takeaways

1

No production Node story exists today - both projects use Bun-native primitives end-to-end: Bun.serve (HTTP + WebSockets), SQL (DB), S3Client, RedisClient, Cookie, Bun.Archive, Bun.Image, Bun.markdown. Porting to Node means swapping ~10 Bun globals for npm packages; the biggest rewrites are Bun.servehttp, SQL→per-dialect drivers (SQLite via the built-in node:sqlite, MySQL via mysql2, Postgres via pg), and Bun.file/Bun.writefs/promises.

2

Node-compat is used heavily already - process.*, node:path, crypto.randomUUID and node:os run natively under Bun, so those parts would be free on Node.

3

Named from "bun" imports (Bun 1.2+ style) carry the production data layer: SQL, S3Client, RedisClient, redis, Cookie, BunRequest, file, write, spawnSync, Glob, ServerWebSocket.