Engineering analysis · Bun vs Node

Bun APIs, mapped to Node

An API-level breakdown of every Bun primitive used by the reepolee-dev framework and the ree-web 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-dev

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.

  • 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
  • ~90 test files on bun:test
ree-web

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

Bun API Where used Node equivalent
Bun.env ~250 usages across both projects (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+)
Bun.argv Process arguments incl. script path (--dev / --prod / --agent / --test in server.ts and scripts/*.ts) process.argv.slice(2), or node:util parseArgs() (stable v20+)
Bun.main Entry-file path checks (scripts/check_domain_compliance.ts) require.main === module (CJS) or compare process.argv[1]
Bun.which() Locate executables in PATH (ree-web/scripts/test_replay.ts) which npm package, or child_process.execSync("which …")
Bun.sleep() Async sleep in smoke tests and dev scripts node:timers/promisessetTimeout(ms)
Bun.color() ANSI terminal colors in generator output node:util styleText() (stable v22.11+), or chalk / picocolors
Bun.stdin.stream() Async stdin iteration (MCP JSON-RPC server) process.stdin async iteration
process.* (Node compat) 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.
Bun API Where used Node equivalent
Bun.serve() lib/server_startup.ts, server.reeman.ts, dev/preview/publisher scripts, tests node:http.createServer() or Express/Fastify/Hono - not drop-in
Bun.serve({ routes }) Production Radix-tree routing in server.ts / server.reeman.ts No built-in; router frameworks (Express, Hono, Fastify)
Bun.Server<T> .port, .upgrade(), .reload() in lib/bootstrap.ts, server.ts node:http.Server (no upgrade helper - see ws)
BunRequest ~40 imports in routes_reeman/**, lib/crud_routes.ts, lib/middleware/* node:http.IncomingMessage + manual URL/body parsing
Bun.WebSocketHandler<T> WS open/message/close config in lib/bootstrap.ts, server.ts ws npm package (WebSocketServer)
server.upgrade() WS upgrade from fetch handler (/__reload livereload) ws + http upgrade event (wss.handleUpgrade)
ServerWebSocket<T> Dev live-editing WS in ree-web/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)
new Response(Bun.file()) Stream a file as the response body (scripts/preview.ts, dev responses) fs.createReadStream() piped to the response

3. File system

Bun API Where used Node equivalent
Bun.file(path) ~150 usages - lazy file handle node:fs/promises.open() / fs.readFile
Bun.file().text() Read as string (init scripts, template engine, install) fs.promises.readFile(path, "utf8")
Bun.file().json() Read and parse JSON (package.json, translations) JSON.parse(await fs.promises.readFile(path, "utf8"))
Bun.file().bytes() Read as bytes (pack-addon tests, asset sync) fs.promises.readFile(path)Buffer
Bun.file().arrayBuffer() Read as ArrayBuffer (image pipeline) fs.promises.readFilebuf.buffer
Bun.file().exists() Existence checks (bundle cache, generators) fs.promises.access(path) or fs.existsSync
Bun.file().stat() File metadata (env sync scripts) fs.promises.stat(path)
Bun.file().delete() Delete a file (lib/bundle_cache.ts) fs.promises.unlink(path)
Bun.write(path, data) ~100 usages - write files and directories fs.promises.writeFile + mkdir(…, { recursive: true })
Bun.Glob 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

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

5. Bundler & transpiler

Bun API Where used Node equivalent
Bun.build() Server-side JS bundling (lib/bundle_cache.ts) esbuild / rollup / webpack npm
Bun.Transpiler TS→JS transpile (ree-web/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

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

7. Data formats (markdown / YAML)

Bun API Where used Node equivalent
Bun.markdown.html() Markdown → HTML in template helpers, the .ree compiler, SSG marked / markdown-it / remark npm (no built-in)
Bun.YAML.parse() YAML frontmatter (ree-web/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.
Bun API Where used Node equivalent
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
Tagged-template SQL db_cli`SELECT …` built-in query interpolation sql-template-strings, pg tagged templates, or Knex/Prisma

9. Object storage - bun:s3

Bun API Where used Node equivalent
S3Client (from "bun") lib/s3/core.ts, lib/issue_reporter.ts (Cloudflare R2) @aws-sdk/client-s3 npm (R2/S3-compatible)
S3File / S3Options Lazy file handles: .write(), .presign(), .exists() @aws-sdk/lib-storage + Upload/GetObjectCommand

10. Redis - bun:redis

Bun API Where used Node equivalent
RedisClient (from "bun") Queue worker, session store, rate-limit store ioredis / redis npm packages
redis (from "bun") Singleton for cache and feature flags ioredis default client instance

11. Cookies

Bun API Where used Node equivalent
Cookie (from "bun") Parse/serialize cookie headers (lib/cookies.ts, auth routes) cookie npm package (parse/serialize)

12. Archive & images

Bun API Where used Node equivalent
Bun.Archive tar create/read (pack_addon.ts, install_archive.ts) tar npm package (no built-in)
Bun.Image Native image processing (lib/s3/proxy.ts, prepare_images.ts) sharp npm package (or jimp); vips CLI

13. import.meta - module metadata

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

14. Test runner - bun:test

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

15. CLI / runtime flags

Bun API Where used Node equivalent
bun run <script> Package.json script runner in both projects npm run
bunx Run npm binaries (bunx tsc in typecheck_ratchet) npx
bun --hot In-place hot reload without process restart (server.ts dev path) node --watch (Node 18.11+; restarts the process)
bun.lock 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 - ree-web production runtime

ree-web 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).
Bun API Where used Node equivalent
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 (routes/*.ree, src/public/*.ree), compiled by lib/template/compiler.ts.
Syntax Meaning Example
{#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.