1. Overview & Questions (SQ3R · Survey & Question)

SQ3R Step 1: Survey the big picture, formulate key questions.

What is Bun?

Bun is an all-in-one toolkit for JavaScript and TypeScript applications. It ships as a single, dependency-free executable called bun. At its core is the Bun runtime — a fast JavaScript runtime designed as a drop-in replacement for Node.js. It's written in Zig and powered by Apple's JavaScriptCore engine under the hood, dramatically reducing startup times and memory usage.

Bun is more than just a runtime. It also includes:

  • Runtime: Execute JavaScript/TypeScript files with near-zero overhead — TS and JSX supported out of the box
  • Package Manager: Fast installs, workspaces, overrides, and audits with bun install
  • Test Runner: Jest-compatible, TypeScript-first tests with snapshots, DOM testing, and watch mode
  • Bundler: Native bundling for JS/TS/JSX with code splitting, plugins, and HTML imports
bun run index.tsx       # TS and JSX supported out of the box
bun install <pkg>       # install a package
bun build ./index.tsx   # bundle a project for browsers
bun test                # run tests
bunx cowsay 'Hello!'    # execute a package

Key Questions

Before diving in, keep these questions in mind:

  1. What fundamentally differentiates Bun from Node.js? Why switch?
  2. Why is the Bun runtime faster than Node.js?
  3. How does Bun's package manager compare to npm/yarn/pnpm?
  4. How do you migrate an existing Node.js project to Bun?
  5. Can Bun's bundler replace webpack/esbuild?
  6. Can Bun's test runner replace Jest?
  7. Is Bun mature enough for production use?

Technology Landscape

Bun Toolkit
├── Runtime
│   ├── JavaScriptCore engine (from Safari)
│   ├── Transpiler and runtime written in Zig
│   ├── Native TypeScript / JSX support
│   ├── ESM + CommonJS compatibility
│   └── Web-standard APIs (fetch, WebSocket, ReadableStream)
├── Package Manager
│   ├── bun install (25x faster than npm)
│   ├── Workspaces / monorepo support
│   ├── Global cache + hardlink/clonefile
│   └── Secure: lifecycle scripts disabled by default
├── Test Runner
│   ├── Jest-compatible API
│   ├── Snapshot testing / lifecycle hooks
│   ├── Watch mode / coverage reporting
│   └── DOM testing (happy-dom)
└── Bundler
    ├── JS/TS/JSX/CSS/HTML
    ├── Code splitting / Tree shaking
    ├── Plugin system
    ├── Single-file executables
    └── Fullstack dev server

2. Explained Simply (Feynman Technique)

If you can't explain something in simple language, you don't truly understand it.

Core Concepts Explained

Bun is Like a Swiss Army Knife

Imagine you're a carpenter. Before, you needed a hammer (Node.js), a toolbox (npm), a testing tool (Jest), and a bundling machine (webpack). Now, Bun is like a Swiss Army knife — one tool does everything. You don't need to install and maintain four separate tools; a single bun command is enough.

Runtime = Engine + Environment APIs

JavaScript (formally, ECMAScript) is just a specification for a programming language. A JavaScript engine takes valid code and executes it (like Google's V8 or Apple's JavaScriptCore). But code needs to interact with the outside world — reading files, making network requests — which is where runtimes come in.

  • Browser runtimes expose APIs through the window object
  • Node.js provides Node-specific globals like Buffer, process, and modules like node:fs, node:http
  • Bun provides Node.js-compatible APIs alongside Web-standard APIs
// Bun supports both Node.js style and Web-standard APIs
import { readFileSync } from "fs"; // Node.js API
const response = await fetch("https://api.example.com"); // Web-standard API

Package Manager = A Faster npm

# npm install: ~170ms startup time
npm install
 
# Bun install: ~6ms startup time — 28x faster
bun install

Why so fast? Bun uses a global package cache combined with hardlinks (Linux) or clonefile (macOS), avoiding redundant downloads and file copies.

Test Runner = A Faster Jest

import { expect, test } from "bun:test";
 
test("2 + 2", () => {
  expect(2 + 2).toBe(4);
});

The API is nearly identical to Jest, but tests run faster because they execute directly in the Bun runtime, eliminating Node.js startup overhead.

Bundler = A Faster esbuild

bun build ./index.tsx --outdir ./out

Bun's bundler outperforms esbuild on its own three.js benchmark. It supports JS/TS/JSX/CSS/HTML out of the box.

Analogies and Metaphors

ConceptAnalogy
Bun RuntimeA faster simultaneous interpreter (JavaScriptCore starts 4x faster than V8)
Bun Package ManagerA courier with a global warehouse (cache + hardlinks, no re-downloading)
Bun Test RunnerA student exam system that uses Jest syntax but runs faster
Bun BundlerA packing machine that bundles scattered files into one package
bunfig.tomlBun's "settings panel," like .npmrc for npm

Common Misconceptions Clarified

Myth 1: "Bun only works on macOS/Linux" Reality: Bun fully supports Windows. On Windows, Bun uses its own shell to support bash-like syntax and common commands.

Myth 2: "Bun is incompatible with the Node.js ecosystem" Reality: Bun aims for full compatibility with Node.js. It supports built-in modules (fs, path, http, etc.) and globals (process, Buffer). This is an ongoing effort, but most npm packages run directly in Bun.

Myth 3: "Bun isn't suitable for production" Reality: Bun is used in production by many companies and supports deployment to AWS Lambda, Google Cloud Run, Vercel, Railway, and other major platforms.

Myth 4: "Using Bun means you can't use npm packages" Reality: Bun's package manager is fully compatible with the npm ecosystem. If your project has a package.json, bun install works with near-zero changes.

3. Cone of Deepening (Simon's Method)

Focused effort, goal-oriented, cone-shaped deepening — start from the core, gradually expand outward.

Layer 1: Core Fundamentals

1. Installing Bun

# macOS / Linux / WSL
curl -fsSL https://bun.sh/install | bash
 
# Windows
powershell -c "irm bun.sh/install.ps1 | iex"
 
# Using npm
npm install -g bun
 
# Using Homebrew
brew install oven-sh/bun/bun
 
# Using Docker
docker pull oven/bun

Verify installation:

bun --version

2. Runtime Basics

The Bun runtime is the core of Bun. It uses the JavaScriptCore engine (developed by Apple for Safari), which in most cases starts faster and runs faster than V8 (used by Node.js).

# Run a file
bun run index.js
bun run index.ts       # TypeScript runs directly
bun run index.tsx      # JSX runs directly
 
# Shorthand: you can omit "run"
bun index.tsx
 
# Run a package.json script
bun run dev
bun run build
 
# Watch mode
bun --watch run index.tsx

Performance comparison (Linux Hello World):

CommandStartup Time
bun hello.js5.2ms
node hello.js25.1ms

Why is it fast? Zig language + JavaScriptCore engine + native transpiler, without Node.js's V8 startup overhead.

3. Package Manager

Bun's package manager is designed as a direct replacement for npm, yarn, and pnpm, with up to 25x speed improvement.

# Install all dependencies
bun install
 
# Add a package
bun add react
bun add react@19.1.1     # specific version
bun add -d typescript     # dev dependency
 
# Remove a package
bun remove react
 
# Global install
bun install -g cowsay
 
# CI/CD mode (frozen lockfile)
bun ci
# equivalent to bun install --frozen-lockfile

Security feature: Bun does not execute lifecycle scripts (like postinstall) of installed dependencies by default, preventing supply chain attacks. To allow scripts for a specific package, declare it in package.json:

{
  "trustedDependencies": ["esbuild", "sharp"]
}

Installation strategies:

# Hoisted mode (traditional npm behavior)
bun install --linker hoisted
 
# Isolated mode (similar to pnpm, prevents phantom dependencies)
bun install --linker isolated

4. Test Runner

Bun ships with a Jest-compatible test runner with TypeScript support, lifecycle hooks, snapshot testing, and DOM testing.

import { expect, test, describe } from "bun:test";
 
describe("Math operations", () => {
  test("addition", () => {
    expect(1 + 1).toBe(2);
  });
 
  test("async test", async () => {
    const data = await fetch("/api/data");
    expect(data.ok).toBe(true);
  });
});
# Run all tests
bun test
 
# Run a specific test file
bun test ./test/math.test.ts
 
# Filter tests by name
bun test --test-name-pattern "addition"
 
# Watch mode
bun test --watch
 
# Concurrent execution
bun test --concurrent
 
# Coverage report
bun test --coverage

Test file patterns: *.test.{js|jsx|ts|tsx}, *_test.*, *.spec.*, *_spec.*

Lifecycle hooks:

import { beforeAll, beforeEach, afterEach, afterAll, test } from "bun:test";
 
beforeAll(() => {
  /* runs once before all tests */
});
beforeEach(() => {
  /* runs before each test */
});
afterEach(() => {
  /* runs after each test */
});
afterAll(() => {
  /* runs once after all tests */
});

Mocking:

import { test, expect, mock } from "bun:test";
 
const random = mock(() => Math.random());
 
test("mock test", () => {
  const val = random();
  expect(random).toHaveBeenCalled();
  expect(random).toHaveBeenCalledTimes(1);
});

5. Bundler

Bun's bundler is a native implementation that outperforms esbuild on its own benchmarks.

# CLI usage
bun build ./index.tsx --outdir ./out
bun build ./index.tsx --outdir ./out --target browser
bun build ./index.tsx --outdir ./out --watch
// JavaScript API
const result = await Bun.build({
  entrypoints: ["./index.tsx"],
  outdir: "./out",
  target: "browser", // "browser" | "bun" | "node"
});
// result => { success: boolean, outputs: BuildArtifact[], logs: BuildMessage[] }

Single-file executables:

# Compile into a standalone executable
bun build --compile ./index.tsx --outfile myapp
./myapp  # runs directly, no Bun installation needed

Layer 2: Advanced Usage

1. HTTP Server (Bun.serve)

Bun includes a high-performance HTTP server that handles roughly 2.5x more requests per second than Node.js.

const server = Bun.serve({
  port: 3000,
  routes: {
    // Static routes
    "/api/status": new Response("OK"),
 
    // Dynamic routes (with parameters)
    "/users/:id": (req) => {
      return new Response(`Hello User ${req.params.id}!`);
    },
 
    // Per-HTTP method handlers
    "/api/posts": {
      GET: () => {
        const posts = db.query("SELECT * FROM posts").all();
        return Response.json(posts);
      },
      POST: async (req) => {
        const body = await req.json();
        return Response.json({ created: true, ...body }, { status: 201 });
      },
    },
 
    // Wildcard route
    "/api/*": Response.json({ message: "Not found" }, { status: 404 }),
  },
 
  // Fallback for unmatched routes
  fetch(req) {
    return new Response("Not Found", { status: 404 });
  },
});
 
console.log(`Server running at ${server.url}`);

Server management:

// Hot-reload routes (without server restart)
server.reload({
  routes: {
    "/api/version": () => Response.json({ version: "2.0.0" }),
  },
});
 
// Stop the server
await server.stop(); // graceful stop
await server.stop(true); // force stop
 
// Get client IP
const address = server.requestIP(req);

2. File I/O

Bun provides an optimized set of file I/O APIs.

// Read files
const text = await Bun.file("./hello.txt").text();
const buffer = await Bun.file("./data.bin").arrayBuffer();
const json = await Bun.file("./config.json").json();
 
// Check if a file exists
const exists = await Bun.file("./config.json").exists();
 
// Get file info
const stat = await Bun.file("./data.bin").stat();
console.log(stat.size);
 
// Write files
await Bun.write("./output.txt", "Hello, World!");
await Bun.write("./data.bin", new Uint8Array([1, 2, 3]));
 
// Stream writing
const file = Bun.file("./large.txt");
const writable = file.writer();
writable.write("chunk 1\n");
writable.write("chunk 2\n");
writable.end();

3. SQLite (bun:sqlite)

Bun natively implements a high-performance SQLite3 driver, roughly 3-6x faster than better-sqlite3.

import { Database } from "bun:sqlite";
 
const db = new Database(":memory:"); // or "mydb.sqlite"
 
// Create a table
db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)");
 
// Insert data (prepared statement)
const insert = db.prepare("INSERT INTO users (name, email) VALUES ($name, $email)");
insert.run({ $name: "Alice", $email: "alice@example.com" });
insert.run({ $name: "Bob", $email: "bob@example.com" });
 
// Query data
const user = db.query("SELECT * FROM users WHERE name = $name").get({ $name: "Alice" });
// => { id: 1, name: "Alice", email: "alice@example.com" }
 
const allUsers = db.query("SELECT * FROM users").all();
// => [{ id: 1, name: "Alice", ... }, { id: 2, name: "Bob", ... }]
 
// Transactions
const insertMany = db.transaction((users) => {
  for (const user of users) {
    insert.run(user);
  }
});
 
insertMany([
  { $name: "Charlie", $email: "charlie@example.com" },
  { $name: "Diana", $email: "diana@example.com" },
]);

Enable WAL mode for better performance:

db.run("PRAGMA journal_mode = WAL;");

Map query results to classes:

class User {
  name: string;
  email: string;
  get displayName() {
    return this.name.toUpperCase();
  }
}
 
const users = db.query("SELECT name, email FROM users").as(User).all();
console.log(users[0].displayName); // "ALICE"

4. WebSocket

Bun has native WebSocket support, integrated directly into Bun.serve.

const server = Bun.serve({
  port: 3000,
  fetch(req, server) {
    // Upgrade HTTP request to WebSocket
    if (server.upgrade(req)) return;
    return new Response("WebSocket upgrade failed", { status: 500 });
  },
  websocket: {
    open(ws) {
      console.log("Client connected");
      ws.subscribe("chat");
    },
    message(ws, message) {
      // Publish to all clients subscribed to "chat"
      server.publish("chat", message);
    },
    close(ws) {
      console.log("Client disconnected");
      ws.unsubscribe("chat");
    },
  },
});

5. Environment Variables

Bun automatically loads .env files — no need to install dotenv.

// Reads directly; .env file is auto-loaded
const apiKey = process.env.API_KEY;
const dbUrl = process.env.DATABASE_URL;
 
// Load from a specific file
// bun --env-file=.env.production run index.ts

6. Shell Scripting

Bun provides a built-in Shell API for running shell commands from JavaScript.

import { $ } from "bun";
 
// Run a command
const result = await $`echo "Hello, World!"`.text();
console.log(result); // "Hello, World!\n"
 
// Piping
const files = await $`ls`.text();
const filtered = await $`grep .ts <<< ${files}`.text();
 
// With error handling
try {
  await $`git push`;
} catch (e) {
  console.error("Push failed:", e);
}

7. Process Management (Bun.spawn)

// Spawn a child process
const proc = Bun.spawn(["echo", "Hello"], {
  stdout: "pipe",
  stderr: "pipe",
});
 
const text = await new Response(proc.stdout).text();
console.log(text); // "Hello\n"
 
const exitCode = await proc.exited;
console.log(`Exit code: ${exitCode}`);

8. FFI (Foreign Function Interface)

Bun can directly call C library functions without writing native bindings.

const lib = dlopen("./mylib.so", {
  add: {
    args: ["int", "int"],
    returns: "int",
  },
});
 
console.log(lib.symbols.add(1, 2)); // 3

Layer 3: Deep Dive

1. JavaScriptCore vs V8: Why Bun is Faster

Bun uses Apple's JavaScriptCore (JSC) engine instead of Google's V8. Key differences:

FeatureJavaScriptCore (Bun)V8 (Node.js)
Startup speedFaster (~5ms)Slower (~25ms)
Memory usageLowerHigher
JIT compilationMore aggressive optimizationProgressive optimization
DeveloperApple (Safari)Google (Chrome)

Why faster startup? JSC's interpreter has lower startup cost, while V8 needs more time to initialize its JIT compilation pipeline.

2. Advantages of Zig

Bun writes its runtime and transpiler in Zig. Zig's advantages:

  • No hidden control flow: No default exception handling, no implicit allocations — code behavior is fully predictable
  • Comptime execution: Code can be executed at compile time for performance optimization
  • C interop: Zig can directly import C header files without FFI bindings
  • Small binary size: Zig compiles to smaller binaries

3. Node.js Compatibility

Bun aims for full Node.js compatibility. Current status:

Supported Node.js APIs:

  • Built-in modules: fs, path, http, https, crypto, os, net, url, util, stream, buffer, events, child_process, and more
  • Globals: process, Buffer, __dirname, __filename
  • Node-API (N-API): supports native C++ addons
  • CommonJS and ES Modules

Migration guide:

# Step 1: Install Bun in an existing Node.js project
npm install -g bun
 
# Step 2: Install dependencies with Bun (generates bun.lock)
bun install
 
# Step 3: Run scripts with Bun
bun run dev    # replaces npm run dev
bun test       # replaces npm test
 
# Step 4: For scripts using Node.js CLI tools, use the --bun flag
bun run --bun vite

Compatibility notes:

  • This is ongoing work — not all Node.js APIs are implemented
  • Most npm packages run directly
  • Use process.versions.bun to detect if running in Bun

4. Plugin System

Bun provides a unified plugin API that can extend both the runtime and bundler.

const plugin = Bun.plugin({
  name: "my-plugin",
  target: "browser",
  setup(build) {
    build.onLoad({ filter: /\.custom$/ }, (args) => {
      const content = Bun.file(args.path).text();
      return {
        contents: `export default ${JSON.stringify(content)}`,
        loader: "js",
      };
    });
  },
});

5. bunfig.toml Configuration

Bun uses bunfig.toml as its configuration file, supporting runtime, package manager, and test runner settings.

# bunfig.toml
 
# Runtime configuration
[run]
# Preload modules
preload = ["./setup.ts"]
 
# Package manager configuration
[install]
# Install optional dependencies
optional = true
# Install dev dependencies
dev = true
# Installation strategy
linker = "hoisted"
# Minimum release age (supply chain attack prevention)
minimumReleaseAge = 259200
 
# Test configuration
[test]
# Coverage
coverage = true
# Retry count
retry = 3
# Timeout in milliseconds
timeout = 5000

6. Fullstack Development with HTML Imports

Bun supports importing HTML files directly for fullstack application development:

import myReactApp from "./index.html";
 
Bun.serve({
  routes: {
    "/": myReactApp,
    "/api/data": () => Response.json({ message: "Hello" }),
  },
});
  • Development (bun --hot): On-demand bundling with Hot Module Replacement (HMR)
  • Production (bun build): Pre-builds all assets with zero runtime bundling overhead

7. Workers (Multi-threading)

Bun supports the Worker API for multi-threaded applications:

const worker = new Worker(new URL("./worker.ts", import.meta.url));
 
worker.postMessage({ type: "process", data: [1, 2, 3] });
 
worker.onmessage = (event) => {
  console.log("Worker result:", event.data);
};

Workers run on separate threads, sharing I/O resources with the main thread.

4. Key Notes (Cornell Notes)

Key Concept Quick Reference

Cue / KeywordDetailed Notes
What is BunAll-in-one JS/TS toolkit: runtime + package manager + test runner + bundler, single executable
JavaScriptCoreJS engine developed by Apple for Safari, starts ~4x faster than V8
ZigLanguage used for Bun's runtime and transpiler; high performance, no hidden control flow, direct C interop
Node.js compatSupports Node.js built-in modules and globals; most npm packages run directly
ESM + CJSSupports both ES Modules and CommonJS; ESM recommended
Web-standard APIsNative fetch, WebSocket, ReadableStream, Headers, URL implementations
Auto-load .envNo dotenv needed; Bun automatically reads .env files
Secure PMDependency lifecycle scripts not executed by default; must declare in trustedDependencies
WAL modeSQLite Write-Ahead Logging, significantly improves concurrent read/write performance
Single-file exebun build --compile generates standalone executables, no Bun installation required

Core API Quick Reference

API / CommandPurposeExample
bun run <file>Run a JS/TS filebun run index.tsx
bun installInstall dependenciesbun install / bun add react
bun testRun testsbun test --watch
bun buildBundle a projectbun build ./index.tsx --outdir ./out
bunx <pkg>Execute an npm packagebunx cowsay 'Hello'
Bun.serve()Start an HTTP serverBun.serve({ port: 3000, fetch })
Bun.file()File readingawait Bun.file("./a.txt").text()
Bun.write()File writingawait Bun.write("./out.txt", "data")
bun:sqliteSQLite databasenew Database(":memory:")
Bun.spawn()Spawn child processBun.spawn(["echo", "hi"])
Bun.plugin()Register pluginBun.plugin({ setup(build) {...} })
Bun.build()Programmatic bundlingawait Bun.build({ entrypoints })
$Shell commandsawait $echo hello.text()

Section Summary

Bun's core value lies in being all-in-one and fast. It integrates the four most commonly used tools in JavaScript/TypeScript development (runtime, package manager, test runner, bundler) into a single binary, achieving significant performance improvements through the JavaScriptCore engine and Zig language. For existing Node.js projects, Bun provides a highly compatible migration path with near-zero modification required.

5. Review & Practice (SQ3R · Recite & Review)

Key Takeaways

  1. Bun is a drop-in replacement for Node.js: Uses the JavaScriptCore engine, starts 4x faster, 2.5x higher HTTP throughput
  2. Four tools in one: Runtime, package manager, test runner, bundler — a single bun command
  3. Package manager is 25x faster: Global cache + hardlink/clonefile, secure by default (no dependency scripts)
  4. Test runner is Jest-compatible: Nearly identical API, low migration cost
  5. SQLite is built-in: High-performance native driver, 3-6x faster than better-sqlite3
  6. TypeScript with zero config: Run .ts and .tsx files directly, no ts-node needed
  7. Web-standard APIs: fetch, WebSocket, ReadableStream, and more work out of the box

Hands-on Exercises

Exercise 1: Create a Bun project

bun init
# Select "Blank" template, then:
bun add express
bun add -d @types/express

Create index.ts:

import express from "express";
const app = express();
 
app.get("/", (req, res) => {
  res.json({ message: "Hello from Bun!" });
});
 
app.listen(3000, () => console.log("Running on http://localhost:3000"));

Run: bun run index.ts

Exercise 2: Build a REST API with a database

import { Database } from "bun:sqlite";
 
const db = new Database(":memory:");
db.run("CREATE TABLE todos (id INTEGER PRIMARY KEY, text TEXT, done INTEGER DEFAULT 0)");
 
const server = Bun.serve({
  port: 3000,
  routes: {
    "/api/todos": {
      GET: () => {
        const todos = db.query("SELECT * FROM todos").all();
        return Response.json(todos);
      },
      POST: async (req) => {
        const { text } = await req.json();
        db.run("INSERT INTO todos (text) VALUES (?)", [text]);
        return Response.json({ success: true }, { status: 201 });
      },
    },
    "/api/todos/:id": (req) => {
      const todo = db.query("SELECT * FROM todos WHERE id = ?").get(req.params.id);
      if (!todo) return new Response("Not Found", { status: 404 });
      return Response.json(todo);
    },
  },
});

Exercise 3: Write tests

import { test, expect, beforeAll } from "bun:test";
import { Database } from "bun:sqlite";
 
let db: Database;
 
beforeAll(() => {
  db = new Database(":memory:");
  db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
  db.run("INSERT INTO users (name) VALUES ('Alice')");
});
 
test("query user", () => {
  const user = db.query("SELECT * FROM users WHERE name = ?").get("Alice");
  expect(user).toBeDefined();
  expect(user.name).toBe("Alice");
});
 
test("all users", () => {
  const users = db.query("SELECT * FROM users").all();
  expect(users).toHaveLength(1);
});

Run: bun test

Common Pitfalls

  1. --watch flag placement: Use bun --watch run index.ts (correct), not bun run index.ts --watch (incorrect — --watch gets passed to the script)
  2. SQLite $param prefix: By default, binding parameters require the $, :, or @ prefix unless using strict: true mode
  3. Dependency lifecycle scripts don't run: If a package needs postinstall to work, you must manually add it to trustedDependencies
  4. Routes syntax version requirement: The routes object syntax requires Bun v1.2.3+; earlier versions need the fetch callback
  5. Hot reloading isn't automatic: You must use the --hot or --watch flag

Further Reading