Guide
Bun fundamentals explained
A greenfield API project needs a runtime, a package manager, a TypeScript compiler,
a test runner, and a bundler. On a typical Node stack that means five tools in
devDependencies, three config files, and CI steps that each add
thirty seconds. Bun collapses that toolchain into a single binary
written in Zig around Apple’s JavaScriptCore engine: install
dependencies, run TypeScript, serve HTTP, execute Jest-style tests, and bundle for
production — often an order of magnitude faster than the Node/npm equivalent.
This guide covers Bun’s architecture, bun install and workspaces,
Bun.serve, Node.js compatibility limits, built-in SQLite and
file I/O APIs, a Harbor Supply order API worked example, a runtime decision table,
common pitfalls, and a practitioner checklist — alongside our
Node.js fundamentals guide,
Deno fundamentals guide,
and
TypeScript fundamentals guide.
What problem Bun solves
Node.js proved JavaScript belongs on the server, but the developer experience
around Node accumulated friction: npm install on large monorepos
can take minutes; every TypeScript service needs a compile or transpile step;
Jest and esbuild add their own configuration dialects. Teams spend sprint time
tuning webpack aliases instead of shipping features.
Bun’s thesis is speed as a product feature. The runtime
executes JavaScript and TypeScript directly (no separate tsc pass for
development), resolves and installs packages with a native linker written in Zig,
and exposes batteries-included APIs — HTTP server, WebSocket support, SQLite
bindings, file hashing — so small services do not import a dozen micro-libraries.
For teams already on Node, Bun advertises drop-in compatibility:
most npm packages and Node built-in modules work unchanged, letting you swap
node for bun in scripts and measure the delta before
committing to a full migration.
When Bun is a good fit
- Greenfield HTTP APIs where startup latency and install time matter (serverless, edge-adjacent containers, developer laptops).
- Monorepos with heavy CI — faster
bun installandbun testshrink pull-request feedback loops. - Internal tools and CLIs that benefit from native TypeScript execution without a build pipeline.
- Prototyping Node migrations — run existing test suites under Bun to surface compatibility gaps early.
Bun is weaker when you depend on native Node addons compiled against V8-specific ABIs, when you need a decade of production battle scars (Node LTS still wins on compliance questionnaires), or when your platform mandates Deno’s permission sandbox. For security-sensitive multi-tenant services, compare Bun’s full-trust model with Deno’s explicit flags in our Deno guide.
Architecture: JavaScriptCore, Zig, and the event loop
Unlike Node (V8 + libuv) and Deno (V8 + Rust), Bun uses JavaScriptCore
— the engine inside Safari. JSC trades some npm-native-addon compatibility for
tight integration with Bun’s Zig standard library: syscalls, networking, and
filesystem code paths avoid extra FFI hops. Bun implements a Node-compatible event
loop (timers, setImmediate, microtasks) so libraries like Express and
Fastify behave as they do on Node, though edge cases exist in older callback-heavy
code.
TypeScript and JSX are first-class: Bun strips types and transforms JSX at parse time
using its own frontend, so bun run server.ts works without
ts-node or tsx. For production bundles, bun build
tree-shakes ESM and CJS inputs into a single file suitable for containers or
AWS Lambda-style deployments.
Memory and startup profiles differ from V8. Micro-benchmarks show Bun cold starts and HTTP throughput ahead of Node on many workloads, but your application’s dependency graph dominates real-world numbers — profile before rewriting architecture around theoretical speedups.
Package management: install, lockfiles, and workspaces
bun install reads package.json like npm or Yarn, resolves
the dependency graph, and writes a text-based lockfile
(bun.lock) optimized for diff-friendly reviews. Key behaviors:
- Hoisting and deduplication — similar to npm’s flat
node_modules, with aggressive caching across projects via a global store. - Lifecycle scripts —
postinstallruns by default; audit whether native compile steps are trustworthy in CI. - Workspaces — monorepo
workspacesarrays in the rootpackage.jsonlink local packages withoutnpm linkceremony. - Catalogs (Bun 1.1+) — centralize version pins for shared dependencies across workspace packages.
{
"name": "harbor-supply-api",
"type": "module",
"scripts": {
"dev": "bun --watch src/server.ts",
"start": "bun src/server.ts",
"test": "bun test",
"build": "bun build src/server.ts --outdir dist --target bun"
},
"dependencies": {
"hono": "^4.6.0",
"zod": "^3.23.8"
}
}
Commit bun.lock to version control. In CI, pin the Bun version
(oven-sh/setup-bun@v2 with an explicit bun-version) so
lockfile format changes do not surprise deploy pipelines. For mixed teams still on
npm, Bun can consume package-lock.json, but standardize on one package
manager per repo to avoid drift.
HTTP servers, routing, and built-in APIs
Bun.serve() exposes a high-performance HTTP and HTTPS server on
web-standard Request/Response objects:
import { Hono } from "hono";
const app = new Hono();
app.get("/health", (c) => c.json({ ok: true }));
export default {
port: 3000,
fetch: app.fetch,
};
// Development: bun --watch src/server.ts
// Bun auto-starts the default export when it exposes port + fetch
Frameworks like Hono (lightweight, runs everywhere), Elysia (Bun-native with end-to-end type inference), and Express (via Node compatibility) layer routing on top. Bun also ships:
bun:sqlite— synchronous and async SQLite without a native addon compile step.Bun.file()/Bun.write()— optimized file reads and streaming responses.Bun.password— Argon2 and bcrypt hashing built in.Bun.redis(experimental) — Redis client withoutiorediswhen your stack allows it.
For WebSocket-heavy apps, Bun’s native server supports upgrade handling; compare with our WebSockets and SSE guide for protocol semantics that stay identical across runtimes.
Testing, bundling, and Node compatibility
bun test implements a Jest-compatible API (describe,
it, expect, mocks, snapshots) with faster collection
and execution. Point it at existing test files — many suites run unmodified.
Coverage reporting integrates via bun test --coverage.
bun build bundles TypeScript, JSX, and CSS for browsers or the Bun
runtime target. Use it for CLI tools and single-file server artifacts; for complex
front-end apps, Vite or Next.js may still be the better fit (see our
Next.js guide).
Node compatibility covers most of node:fs,
node:crypto, node:stream, and node:http, but
gaps remain: some native modules built for Node’s V8 version fail to load;
obscure process flags and deprecated APIs may behave differently.
Maintain a compatibility matrix in your repo: run bun test and a staging
smoke suite before promoting Bun to production entrypoints.
Worked example: Harbor Supply order API
Harbor Supply’s warehouse team needed a small REST service that accepts purchase orders from the storefront, validates line items against inventory, and writes rows to SQLite for nightly ERP export. Requirements: sub-100 ms p95 on a single VPS, TypeScript without a separate compile step, and fast CI on a 40-package monorepo.
Architecture
- Runtime — Bun 1.2.x behind nginx TLS termination on port 3000.
- Router — Hono with Zod request validation (
zValidatormiddleware). - Storage —
bun:sqlitewith WAL mode for concurrent reads during writes. - Auth — API key in
Authorizationheader; keys hashed withBun.password. - Observability — structured JSON logs per request; health endpoint for load balancer probes.
Core handler sketch:
import { Database } from "bun:sqlite";
import { Hono } from "hono";
import { z } from "zod";
const db = new Database("orders.sqlite", { create: true });
db.run(`CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY, sku TEXT, qty INTEGER, created_at TEXT
)`);
const orderSchema = z.object({
sku: z.string().min(1),
qty: z.number().int().positive(),
});
const app = new Hono();
app.post("/orders", async (c) => {
const body = orderSchema.parse(await c.req.json());
const id = crypto.randomUUID();
db.run("INSERT INTO orders VALUES (?, ?, ?, ?)",
[id, body.sku, body.qty, new Date().toISOString()]);
return c.json({ id }, 201);
});
CI runs bun install --frozen-lockfile, bun test (unit
tests with in-memory SQLite), and bun build for a single deploy
artifact. Install time dropped from 94 seconds (npm) to 11 seconds (Bun) on the
monorepo root; test suite wall clock fell 40%. They kept Node LTS for the legacy
ERP bridge service that depends on a V8-only native module — a clean split
documented in the runtime decision table below. Idempotency for duplicate POST
retries follows patterns in our
idempotency guide.
Runtime decision table
| Runtime | Best for | Trade-offs |
|---|---|---|
| Bun | Fast installs/tests, native TypeScript APIs, SQLite-heavy microservices, Node migration experiments | Younger production history; JavaScriptCore native-addon gaps; no permission sandbox |
| Node.js LTS | Maximum ecosystem compatibility, native addons, enterprise support contracts | Slower installs and test runs; TypeScript needs tooling |
| Deno | Security-sensitive services, edge deploys, JSR-first greenfield APIs | Smaller package surface; different module conventions |
| Serverless managed | Per-request billing, global anycast, zero server patching | Cold starts, CPU/time limits, vendor-specific APIs |
Common pitfalls
- Assuming 100% Node parity — native modules and obscure
node:APIs break silently; run integration tests under Bun before switching production entrypoints. - Ignoring
bun.lockin git — non-reproducible CI installs and surprise dependency drift. - Running untrusted
postinstallscripts — Bun executes lifecycle scripts by default; use--ignore-scriptsin locked-down pipelines when appropriate. - Blocking the event loop — synchronous CPU work or huge SQLite transactions still stall concurrent requests; offload heavy compute.
- Mixing package managers — npm and Bun lockfiles diverge; pick one per repository.
- Skipping version pins in CI — Bun releases frequently; unpinning breaks builds when lockfile formats change.
- Using Bun for every service blindly — keep Node for legacy native dependencies until a compatibility path exists.
Practitioner checklist
- Pin Bun version in CI (
oven-sh/setup-bunwith explicit semver) and document local install viacurl -fsSL https://bun.sh/install | bashor mise. - Commit
bun.lock; usebun install --frozen-lockfilein deploy pipelines. - Run
bun teston every pull request; compare results with Node until migration is complete. - Prefer ESM (
"type": "module") and web-standardfetchfor portability. - Place services behind a reverse proxy for TLS, rate limiting, and request size caps.
- Validate request bodies with Zod or similar before database writes; return consistent error shapes.
- Enable SQLite WAL and periodic backups for embedded databases; do not treat SQLite as a multi-writer cluster store.
- Log structured JSON with request IDs; handle
SIGTERMby draining in-flight HTTP before exit. - Re-run native-addon compatibility quarterly as Bun’s Node layer matures.
- Document which services stay on Node LTS and why — avoids accidental unified migrations.
Key takeaways
- Bun is an all-in-one JavaScript runtime: package manager, test runner, bundler, and TypeScript executor in one binary.
- JavaScriptCore and Zig underpin speed-focused I/O; Node compatibility covers most npm workloads but not all native addons.
bun install,bun test, andBun.servereplace multiple devDependencies for small services.- Built-in
bun:sqliteand password hashing suit embedded-data microservices without compile-time native deps. - Adopt Bun where velocity wins; keep Node LTS where compliance, native modules, or Deno-style sandboxing dominate.
Related reading
- Node.js fundamentals explained — V8, libuv, npm, and the event loop Bun aims to replace
- Deno fundamentals explained — secure-by-default runtime with explicit permission flags
- TypeScript fundamentals explained — types and strict mode that Bun executes natively
- REST API design explained — routes, status codes, and versioning for Bun HTTP services