Bun Runtime — Fast All-in-One JavaScript Runtime

Status: Active | Last Updated: 2026-08-29 Category: Runtime — Bun Prerequisites: TypeScript basics; command-line basics Tags: bun, runtime, server, install, build Estimated Time: 2–3 hours (Self-paced, includes lab time)

Summary

Bun is the runtime used by this project (bun.lock in repo root). It is faster than Node at install, start, test, and build. Bun also includes a native TypeScript compiler, so you don't need tsc for development — but you should understand when Bun's compiler is enough and when tsc is required for production builds.

What You'll Learn (Core Competencies)

Table of Contents

  1. Architectural Overview & Core Schema
  2. Deep Dive & Implementation
  3. Anti-Patterns & Common Pitfalls
  4. Independent Challenge
  5. Consolidation & Key Invariants
  6. Next Steps

1. Architectural Overview & Core Schema

Foundational theory, system mechanics, and high-level design.

What is Bun

Bun is a JavaScript runtime built from scratch in Zig. It uses the JavaScriptCore engine (the same engine Safari uses) rather than V8 (Node's engine). Bun aims to be an all-in-one toolkit: runtime, package manager, test runner, bundler, and server framework. The core philosophy is that modern JavaScript development involves too many separate tools — a package manager, a TypeScript compiler, a test runner, a bundler, and a dev server. Bun collapses these into a single binary that starts in under a second, installs packages in a fraction of the time Node's npm takes, and runs TypeScript without a separate compilation step. This makes it especially useful for development workflows where iteration speed matters, and for serverless environments where cold-start latency is a concern.

Bun's SQLite integration is one of its standout features. Rather than requiring you to install a separate database driver or manage a database server, Bun ships with Bun.sql which exposes a fast, synchronous SQLite API backed by libsql (a fork of SQLite). This is particularly powerful for local development, prototyping, and edge functions where you want persistence without infrastructure overhead. The API mirrors the familiar JDBC-style parameter binding pattern:

import { Database } from "bun:sqlite";

const db = new Database("app.db");

db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id    INTEGER PRIMARY KEY AUTOINCREMENT,
    name  TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
  )
`);

// Synchronous — no async/await needed
const stmt = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
stmt.run("Alice", "alice@example.com");

// Query with automatic type coercion
const rows = db.query("SELECT * FROM users WHERE name = ?").all("Alice") as {
  id: number;
  name: string;
  email: string;
}[];
console.log(rows);

The fact that SQLite operations are synchronous is not a performance problem in Bun — the runtime's I/O model means blocking calls yield efficiently to the event loop, and libuv's thread pool handles any truly blocking operations without freezing the main thread.

Bun also ships with Bun.file() for fast, zero-copy file reading and Bun.$() for shell command execution that feels like a first-class API. Bun.file() maps files as memory-backed buffers and can stream them directly to HTTP responses with minimal copying. Bun.$() wraps spawn with a Promise-based interface, making it natural to call shell commands from within TypeScript:

// Fast file reading — no fs.readFileSync needed
const file = Bun.file("package.json");
const content = await file.text();
const size = file.size;

// Shell execution as a Promise
const result = await Bun.$`git status --short`.text();
console.log(result);

// Piped shell commands work too
const piped = await Bun.$`echo "hello" | tr 'a-z' 'A-Z'`.text();
console.log(piped); // HELLO

Bun's HTTP client (fetch), WebSocket server, and TCP/UDP socket APIs are also built-in — you do not need to install node-fetch, ws, or net separately. This makes Bun scripts self-contained in a way that Node scripts rarely are.


2. Deep Dive & Implementation

Technical implementation, fully commented code blocks, and step-by-step logic.

Installing Bun

Bun installs as a single binary, similar to Node itself. The official installation script is the recommended approach on Linux and macOS. Windows support is available through WSL2 or the Bun Windows installer in preview.

# Check whether Bun is already installed
bun --version

# If missing: install via the official script (sets up ~/.bun)
curl -fsSL https://bun.sh/install | bash

# After installation, restart your shell or source the profile
# The script prints the exact command to run, e.g.:
source ~/.bashrc   # or ~/.zshrc, ~/.profile

# Verify installation
bun --version      # e.g. 1.2.x

# Keep Bun up to date — Bun auto-updates itself
bun upgrade

On macOS you can also use Homebrew:

brew install bun
brew upgrade bun

On Linux, if you prefer a version manager similar to nvm, you can use bun-plugin or simply replace the binary after downloading a specific release:

# Install a specific version
curl -fsSL https://github.com/oven-sh/bun/releases/download/bun_v1.2.3/bun-linux-x64.zip -o /tmp/bun.zip
unzip /tmp/bun.zip -d ~/.bun
export PATH="$HOME/.bun/bin:$PATH"

The bun binary is self-contained. It does not require a separate runtime, interpreter, or system library beyond the standard C library. Bun ships its own JavaScriptCore engine and SQLite bindings statically linked into the binary, so there are no additional runtime dependencies to manage.

Basic Commands

Bun's CLI covers the full development lifecycle. The four commands you will use most often are bun install, bun run, bun test, and bun build.

bun install

Installs all dependencies from package.json (and bun.lock, if present). It is typically 5–10x faster than npm install because it uses a parallel, concurrent resolution algorithm and avoids npm's package-lock overhead. It also deduplicates the lockfile format to a single bun.lock file (a binary format that bun.lockb can inspect).

# Install all dependencies
bun install

# Install a specific package and save to package.json
bun add express          # default: dependencies
bun add -D typescript    # devDependencies
bun add -g bun           # global install

# Remove a package
bun remove express

# List installed packages
bun pm ls

Bun's lockfile (bun.lock / bun.lockb) is deterministic. Commit it to version control — it pins exact versions just like package-lock.json or pnpm-lock.yaml, but reads and writes are significantly faster. The bun.lockb file is the binary format; bun.lock in JSON format is a human-readable alternative when BUN_INSTALL_CONVERT_LOCKFILE=1 is set.

bun run

Executes a script defined in package.json or any file directly. Bun runs the script in its runtime, which means TypeScript files work without a separate compilation step. This is the primary way to run your application during development.

# Run a script defined in package.json
bun run dev
bun run build
bun run lint

# Run a file directly (TypeScript works out of the box)
bun run src/index.ts
bun run scripts/migrate.ts

# Watch mode (restarts on file changes)
bun --watch run dev

When you run bun run, Bun uses its built-in bundler to resolve and load the entry point. For most development scenarios this is seamless. However, note that Bun's runtime transpilation is for execution — it is not a full TypeScript type checker. For type-checking before production builds, you still need tsc --noEmit.

bun test

Bun has a built-in test runner that is compatible with Jest, Vitest, and Bun's own describe/test/expect API. The test runner is written in Zig and is significantly faster than Jest or Vitest for large test suites. It supports snapshots, mocks, spying, and concurrent test execution.

# Run all tests
bun test

# Run a specific test file
bun test src/auth.test.ts

# Run tests matching a name pattern
bun test --test-name-pattern "login"

# Watch mode during development
bun test --watch

# Run with coverage (using Bun's built-in reporter)
bun test --coverage

Example test file using Bun's native test API:

// src/greet.test.ts
import { describe, test, expect } from "bun:test";
import { greet } from "./greet";

describe("greet()", () => {
  test("returns a greeting with the given name", () => {
    expect(greet("World")).toBe("Hello, World!");
  });

  test("defaults to 'Guest' when no name is given", () => {
    expect(greet()).toBe("Hello, Guest!");
  });
});
// src/greet.ts
export function greet(name = "Guest"): string {
  return `Hello, ${name}!`;
}

Run with bun test src/greet.test.ts. Bun's test runner also accepts Vitest-style configuration in vitest.config.ts, making migration from Vitest straightforward.

bun build

Bundles JavaScript and TypeScript for distribution. Bun's bundler is written in Zig and produces output for the browser, Node.js, or Bun itself. It supports CommonJS, ESM, and JSX/TSX out of the box, and handles loaders for CSS, TOML, and other file types.

# Bundle for the browser (default: ESM output)
bun build ./src/index.ts --outdir ./dist

# Bundle as a single self-contained file
bun build ./src/index.ts --outfile ./dist/bundle.js --minify

# Bundle for Node.js (CJS output)
bun build ./src/index.ts --outfile ./dist/index.cjs --target node

# Bundle with a specific loader for non-standard extensions
bun build ./src/app.tsx \
  --outfile ./dist/app.js \
  --loader .md:md

For production deployments in this project, bun build is used to produce optimised client-side bundles. The server-side code runs directly via bun run without bundling, which avoids an extra build step and keeps cold-start times short.

Native TypeScript Support

Bun executes TypeScript (.ts, .tsx, .mts) and JSX/TSX files directly without a separate compilation step. It transpiles TypeScript to JavaScript in-memory as part of the module-loading process. This means you can run bun index.ts and it works, with no tsc required.

// src/user.ts — valid TypeScript, runs with `bun src/user.ts`
interface User {
  id: number;
  name: string;
  email: string;
  createdAt: Date;
}

function formatUser(user: User): string {
  return `[${user.id}] ${user.name} <${user.email}>`;
}

const alice: User = {
  id: 1,
  name: "Alice",
  email: "alice@example.com",
  createdAt: new Date(),
};

console.log(formatUser(alice));

Bun handles type erasure automatically — it strips TypeScript types at load time. However, Bun is not a type checker. It does not report type errors like tsc does. For production code, always run tsc --noEmit in your CI pipeline:

# Type-check without emitting files
bun tsc --noEmit

# Or invoke the project's tsc directly
npx tsc --noEmit

The distinction matters: bun run is fast and developer-friendly; tsc --noEmit is the gatekeeper that catches type errors before they reach production.

Bun also supports .tsx and .jsx files with automatic JSX transformation:

// src/Hello.tsx
import React from "react";

interface Props {
  name: string;
  count?: number;
}

export function Hello({ name, count = 0 }: Props) {
  return (
    <div>
      <h1>Hello, {name}</h1>
      <p>Count: {count}</p>
    </div>
  );
}

Run bun src/Hello.tsx and Bun handles the React/JSX transpilation automatically. No Babel, no SWC, no extra config needed for standard JSX usage.

Bun.serve() — Native HTTP

Bun.serve() is Bun's built-in HTTP server API. It is not a wrapper around Node's http module — it is a first-class, Zig-implemented HTTP server that bypasses libuv for I/O entirely. This makes it one of the fastest HTTP server options available in the JavaScript ecosystem, routinely outperforming Express on Node.js by a significant margin.

Basic Server

// server.ts
Bun.serve({
  port: 3000,
  fetch(request) {
    const url = new URL(request.url);

    if (url.pathname === "/health") {
      return new Response("OK", { status: 200 });
    }

    if (url.pathname === "/api/users" && request.method === "GET") {
      return Response.json([
        { id: 1, name: "Alice" },
        { id: 2, name: "Bob" },
      ]);
    }

    return new Response("Not Found", { status: 404 });
  },
  error(error) {
    console.error(error);
    return new Response("Internal Server Error", { status: 500 });
  },
});

console.log("Server running on http://localhost:3000");

Run with bun run server.ts. The server starts in a few milliseconds — there is no need for node --require ts-node/register or any transpilation pipeline.

Route Parameters and JSON Body Parsing

Bun.serve({
  port: 3000,
  async fetch(request) {
    const url = new URL(request.url);
    const pathname = url.pathname;

    // GET /users/:id
    const userMatch = pathname.match(/^\/users\/(\d+)$/);
    if (userMatch && request.method === "GET") {
      const userId = userMatch[1];
      return Response.json({ id: userId, name: "User " + userId });
    }

    // POST /users (JSON body)
    if (pathname === "/users" && request.method === "POST") {
      try {
        const body = await request.json() as { name: string; email: string };
        // In production: insert into database
        return Response.json({ id: Date.now(), ...body }, { status: 201 });
      } catch {
        return new Response("Invalid JSON", { status: 400 });
      }
    }

    return new Response("Not Found", { status: 404 });
  },
});

Streaming Responses

For large responses or real-time data, Bun supports streaming bodies:

async function* generateEvents() {
  for (let i = 0; i < 10; i++) {
    await new Promise((r) => setTimeout(r, 500));
    yield `data: event #${i}\n\n`;
  }
}

Bun.serve({
  port: 3000,
  fetch(request) {
    const url = new URL(request.url);
    if (url.pathname === "/events") {
      return new Response(generateEvents(), {
        headers: {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
        },
      });
    }
    return new Response("Not Found", { status: 404 });
  },
});

WebSockets

Bun ships WebSocket support via Bun.websocket:

Bun.serve({
  port: 3000,
  websocket: {
    open(ws) {
      console.log("Client connected");
    },
    message(ws, message) {
      // Echo back
      ws.send(message.toString().toUpperCase());
    },
    close(ws) {
      console.log("Client disconnected");
    },
  },
  fetch(request) {
    return new Response("Upgrade required", { status: 426 });
  },
});

For production API servers, this project uses Bun.serve() directly rather than Express or Fastify. For teams preferring an Express-compatible API, bun install express and the Express middleware ecosystem work correctly under Bun.

Bun vs Node

Bun and Node are both JavaScript runtimes, but they make different trade-offs. Understanding these trade-offs helps you decide which to use in a given context.

Performance

Bun is measurably faster at package installation, cold starts, and HTTP serving. In benchmarks, bun install is typically 5–10x faster than npm install and 2–5x faster than pnpm install. Bun.serve() outperforms Node's HTTP server and Express for raw request throughput. However, for long-running CPU-intensive workloads (crypto, image processing), the difference narrows as the JIT compiler (V8's in Node, JavaScriptCore's in Bun) warms up.

Ecosystem Compatibility

Node has the largest and most mature ecosystem of any JavaScript runtime. Almost every npm package works on Node. Bun is compatible with the Node.js API surface (node:* built-ins, fs, path, http, etc.) and most npm packages work out of the box. However, packages that use native addons (.node files compiled with node-gyp) may not work in Bun without adaptation, since Bun uses JavaScriptCore rather than V8. Packages that rely on V8-specific APIs (e.g., v8.serialize()) also need alternatives.

TypeScript

Both Bun and Node run TypeScript, but in different ways. Bun transpiles TypeScript at load time as part of its module loader. Node does not natively run TypeScript — you need a transpiler like ts-node, tsx, or a bundler. In development, Bun's approach is faster (no pre-transpilation). For CI/CD production builds, Bun's bundler (bun build) produces optimised output, but tsc --noEmit is still recommended for type safety.

When to Use Bun

When to Use Node

This project uses Bun as the primary runtime for development and serverless functions. Node is used as a fallback when a package has Bun-specific compatibility issues or when deploying to environments that only support Node.

Guided Checkpoint

Run the following to verify your Bun environment is fully operational — all commands should complete without errors.

# 1. Confirm Bun version
bun --version

# 2. Run a TypeScript file directly (no tsc needed)
echo 'const msg: string = "Bun is working"; console.log(msg);' > /tmp/bun-check.ts
bun /tmp/bun-check.ts

# 3. Run the built-in test (from the repo root)
cd /home/agentic/fogserv.cloud
bun test --reporter=default 2>&1 | head -20

# 4. Verify bun.lock exists in the repo
ls -la bun.lock 2>/dev/null && echo "bun.lock found" || echo "No bun.lock in root"

3. Anti-Patterns & Common Pitfalls

Documented failure modes and how to detect/prevent them.

1. Treating Bun's transpiler as a type checker

Bun runs TypeScript by transpiling it in-memory at load time. This means Bun strips types and executes the resulting JavaScript — but it never reports type errors. A file that passes bun run can still have runtime type errors, missing properties, or wrong method signatures.

Detection: No errors in development, but tsc --noEmit in CI reports dozens of issues. Prevention: Always run tsc --noEmit in your CI pipeline, even when using Bun. Configure your editor's TypeScript language service to point at the project's tsconfig.json.

# Wrong: shipping without type-checking
bun run build && deploy

# Right: type-check before building
bun tsc --noEmit && bun run build

2. Using Bun-specific APIs in shared code that must also run on Node

Bun ships APIs like Bun.serve(), Bun.sql, Bun.file(), and Bun.$() that do not exist on Node. Code that uses them directly will crash with a ReferenceError when run under Node.

Detection: Runtime referenceError: Bun is not defined when deploying to a Node environment. Prevention: Gate Bun-specific APIs behind a runtime check:

const isBun = typeof Bun !== "undefined";

if (isBun) {
  // Bun-only code
  const file = Bun.file("config.json");
} else {
  // Node fallback
  import fs from "node:fs/promises";
  const content = await fs.readFile("config.json", "utf8");
}

Or use a shared abstraction layer that both runtimes implement.

3. Relying on synchronous SQLite in high-concurrency request handlers

Bun.sql exposes a synchronous API. While Bun's I/O model handles this gracefully in most cases, running synchronous database operations inside Bun.serve() fetch callbacks under very high concurrency can starve the event loop.

Detection: Under load, request latency spikes and the event loop stalls. Prevention: For high-throughput servers, use db.query().all() sparingly inside fetch handlers, or offload heavy database work to a Worker thread.

4. Forgetting to pin the Bun version in CI

Bun auto-updates on bun upgrade, but CI environments can pull a different version on each run if not pinned. Behaviour changes between minor versions (especially around module resolution and built-in APIs) can break builds silently.

Detection: Build works locally but fails in CI, or vice versa. Prevention: Pin the exact version in CI and in project documentation:

# Pin in CI
curl -fsSL https://github.com/oven-sh/bun/releases/download/bun_v1.2.3/bun-linux-x64.zip
# Or use mise/rtx to manage Bun version
mise install bun@1.2.3

5. Misunderstanding the lockfile format

Bun's default lockfile is a binary format (bun.lockb). It is not human-readable and can cause merge conflicts in large teams if everyone treats it as opaque.

Detection: bun.lockb shows as binary in git diff; merge conflicts are difficult to resolve. Prevention: Use bun.lock (JSON format) for projects with many contributors, or establish a team convention to always run bun install after a merge to regenerate the lockfile cleanly.

6. Deploying bun run without a build step to production

Running bun run src/server.ts directly in production means the runtime transpiles TypeScript on every startup and on every module import. For serverless functions this adds measurable cold-start overhead.

Detection: Cold-start times in production profiling are higher than expected. Prevention: Use bun build --target=bun ./src/server.ts --outfile=dist/server for production deployments, then run the compiled output.


4. Independent Challenge

A problem to solve without step-by-step guidance; promotes synthesis.

Challenge: Build a self-contained Bun CLI data pipeline

Problem: Build a single-file Bun script (pipeline.ts) that:

  1. Reads a CSV file (data/input.csv) with columns: id,name,email,score
  2. Filters rows where score >= 60
  3. Sorts by score descending
  4. Writes the results to data/output.csv using Bun.file()
  5. Prints a summary: total rows processed, rows passed filter, average score of passing rows
  6. Uses Bun.$() to run wc -l data/output.csv and report the line count

The script must:

Setup: Create the data/ directory and a sample input.csv yourself.

No solution is provided — verify your output by running bun pipeline.ts and comparing the produced data/output.csv against a manual filter of the input.


5. Consolidation & Key Invariants

Bullet-list summary of the must-remember rules.


6. Next Steps

Sequenced links to dependent lessons, deeper dives, or production guides.


Change Log

All meaningful modifications should be tracked here with a date and session context.

2026-08-29

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse