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
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-treeroutes:in production -
import { SQL } from "bun"- one client for SQLite, MySQL, Postgres -
S3Client,RedisClient,Cookieas namedbunimports -
~90 test files on
bun:test
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.Imagefor responsive WebP/AVIF/JPEG generation -
Bun.markdown.html()andBun.YAML.parse()for content -
cf-worker.tsuses Cloudflare'sHTMLRewriter, not Bun
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/promises → setTimeout(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
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.readFile → buf.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 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
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
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.serve→http, SQL→per-dialect drivers (SQLite via the built-in node:sqlite, MySQL via mysql2, Postgres via pg), and Bun.file/Bun.write→fs/promises.
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.
Named from "bun" imports (Bun 1.2+ style) carry the production data layer: SQL, S3Client, RedisClient, redis, Cookie, BunRequest, file, write, spawnSync, Glob, ServerWebSocket.