cheat sheet

Bun

Bun is a fast all-in-one JavaScript runtime, package manager, bundler, and test runner written in Zig — a drop-in replacement for Node.js and npm with built-in TypeScript and JSX support.

Bun — All-in-One JavaScript Runtime & Toolkit

What it is

Bun is a JavaScript/TypeScript runtime, package manager, bundler, and test runner shipped as a single binary. It's written in Zig, runs on JavaScriptCore (Safari's engine — the same one Node will not use), and is designed to be a drop-in replacement for node, npm, pnpm, tsc, webpack, and vitest all at once. Reach for it when you want native TypeScript/JSX execution with no compile step, dramatically faster bun install (typically 10–25× faster than npm install), or you're starting a green-field project and don't want to wire up five different tools. The closest alternatives are Node.js (the incumbent) and Deno (security-first); Bun's pitch is "Node-compatible but fast".

Install

Bun ships pre-built binaries for Linux, macOS, and Windows. The official one-line installer downloads the right one and drops it in ~/.bun/bin/bun.

bash
# Linux / macOS / WSL — official installer
curl -fsSL https://bun.sh/install | bash

# macOS — Homebrew
brew install oven-sh/bun/bun

# Windows — PowerShell
powershell -c "irm bun.sh/install.ps1 | iex"

# npm (cross-platform, slowest path)
npm install -g bun

Output: (none — exits 0 on success)

Verify the install — bun --version prints the version, bun --revision adds the git SHA.

bash
bun --version

Output:

text
1.2.14

Upgrade in place — Bun manages its own binary and keeps the previous version for rollback.

bash
bun upgrade

Output:

text
Bun v1.2.14 → Bun v1.2.18
Successfully installed Bun v1.2.18!

Syntax

The base shape is a single bun binary that dispatches based on the first argument — bun <file> to run, bun install for packages, bun build to bundle, bun test to test. Most subcommands accept --help for a focused help screen.

bash
bun <subcommand|file> [args] [--flag value]

Output: (none — exits 0 on success)

Essential commands

CommandPurpose
bun <file>Execute a JS/TS/JSX/TSX file directly.
bun run <script>Run a script from package.json.
bun install (bun i)Install dependencies from package.json / bun.lock.
bun add <pkg>Add a package to dependencies.
bun remove <pkg>Remove a package.
bun update <pkg>Update a package within its semver range.
bun build <entry>Bundle for production or library output.
bun testRun the built-in test runner.
bun create <template>Scaffold a new project from a template.
bun initInteractive package.json + tsconfig.json scaffold.
bun x <pkg>Run a CLI from a package (npx equivalent).
bun pm <subcmd>Inspect / clean the package store.
bun upgradeSelf-update the Bun binary.

Running scripts (TypeScript with no compile step)

Bun executes .js, .ts, .jsx, and .tsx directly — no tsc, no ts-node, no Babel. The TypeScript is transpiled in-process by Bun's bundler and cached for subsequent runs.

typescript
// hello.ts — pass --strict directly, no compile step
interface User {
  name: string;
  email: string;
}

const user: User = { name: "Alice Dev", email: "alice@example.com" };
console.log(`Welcome, ${user.name}!`);
bash
bun hello.ts

Output:

text
Welcome, Alice Dev!

Hot-reload with --hot

bun --hot reloads only the changed module while preserving in-memory state — ideal for HTTP servers because connections stay open. bun --watch does a full process restart (closer to node --watch).

bash
# Restart the entire process on change (like nodemon)
bun --watch server.ts

# Hot module replacement — keep state, swap the module
bun --hot server.ts

Output:

text
[bun] watching server.ts and 4 imports
Server listening at http://localhost:3000

Passing arguments

Arguments after the file path land in Bun.argv (alias for process.argv — Bun ships full Node compat).

typescript
// args.ts
console.log(Bun.argv.slice(2));
bash
bun args.ts foo bar --flag

Output:

text
[ "foo", "bar", "--flag" ]

--bun flag (force the Bun runtime under tools)

Some tools shell out to node internally. --bun rewrites the shebang or hijacks the spawn so the child process is also Bun — useful for running Vite, Next.js, or Astro under the Bun runtime instead of Node.

bash
bun --bun vite dev
bun --bun next build

Output: (none — exits 0 on success)

Package management (bun install)

bun install reads package.json, resolves the dependency tree, and writes a binary bun.lock (formerly bun.lockb) — typically 10–25× faster than npm install because resolution is parallelised in Zig and metadata is cached aggressively.

bash
# Install everything from package.json
bun install
bun i               # short alias

# Add a runtime dep
bun add hono

# Add a dev dep
bun add -d vitest typescript

# Add a peer dep
bun add --peer react

# Install exact version (no ^ in package.json)
bun add --exact lodash

# Global install (rare — Bun prefers per-project)
bun add -g typescript

# Remove
bun remove hono

# Update within semver range
bun update
bun update hono     # one package

# Production install — skip devDependencies
bun install --production

Output (bun add hono):

text
bun add v1.2.14

 installed hono@4.6.14

 1 package installed [187.00ms]

Force a clean reproducible install — Bun's equivalent of npm ci. Fails if bun.lock is out of sync with package.json.

bash
bun install --frozen-lockfile

Output: (none — exits 0 on success)

Inspect the installed tree:

bash
bun pm ls

Output:

text
/home/alice/my-app node_modules (15)
├── hono@4.6.14
├── zod@3.23.8
├── typescript@5.4.5
└── @types/node@20.12.7

bun pm — package store utilities

bun pm exposes the internal package store. Useful for inspecting versions, pruning unused tarballs, and locating the global cache.

bash
bun pm ls --all              # full dependency tree
bun pm cache                 # print cache path
bun pm cache rm              # nuke the cache
bun pm pack                  # build a tarball ready to publish
bun pm whoami                # print logged-in npm user
bun pm bin                   # path to local node_modules/.bin

Output (bun pm cache):

text
/home/alice/.bun/install/cache

bunx — run a CLI without installing

bun x (alias bunx) downloads a package's CLI and executes it — the same role as npx, but cached by Bun's store and dramatically faster on repeat invocations.

bash
# Run create-vite without installing
bunx create-vite@latest my-app

# Run a one-off tool
bunx prettier --check src

# Pin a version
bunx serve@14.2.1 .

Output: (none — exits 0 on success)

bun build — the bundler

bun build is a JS/TS bundler that competes with esbuild and webpack. It outputs ESM, CJS, IIFE, or standalone executables, and can target browser, node, or bun runtimes. Tree-shakes by default and supports code-splitting.

bash
# Bundle a Worker / serverless entry
bun build ./src/index.ts --outfile=dist/index.js

# Multiple entries with code splitting
bun build ./src/index.ts ./src/admin.ts --outdir=dist --splitting

# Target the browser, minified
bun build ./src/main.ts --outdir=dist --target=browser --minify

# Produce a standalone executable (Bun runtime + your code in one binary)
bun build ./src/cli.ts --compile --outfile=mycli

# Cross-compile for Linux ARM64 from a macOS laptop
bun build ./src/cli.ts --compile --target=bun-linux-arm64 --outfile=mycli-linux

Output (bun build ... --minify):

text
  ./dist/main.js  3.42 KB  (entry point)
  ./dist/chunk-Q5J8.js  1.18 KB

[12ms] bundle 2 modules

Bundler options reference

FlagPurpose
--outdir <dir>Directory for emitted files (use with code splitting).
--outfile <path>Single-file output (mutually exclusive with --outdir).
--targetbrowser (default), node, or bun.
--formatesm, cjs, or iife.
--minifyMinify identifiers, whitespace, and syntax.
--sourcemapnone, linked, external, or inline.
--splittingEmit shared chunks (ESM only).
--external <pkg>Mark a dep as external (don't bundle).
--define KEY=valueSubstitute identifiers at compile time.
--compileBake the Bun runtime in and emit a single executable.

bun test — the test runner

bun test is a Jest-compatible test runner with describe, it/test, expect, mocks, snapshots, and lifecycle hooks. It's typically 4–8× faster than Vitest because there's no separate vite build step and tests run on the same in-process transpiler.

typescript
// math.test.ts
import { describe, expect, test, beforeEach } from "bun:test";

function add(a: number, b: number) {
  return a + b;
}

describe("add()", () => {
  let counter = 0;
  beforeEach(() => { counter++; });

  test("adds two positive numbers", () => {
    expect(add(2, 3)).toBe(5);
  });

  test("supports snapshots", () => {
    expect({ name: "Alice Dev", email: "alice@example.com" }).toMatchSnapshot();
  });
});
bash
bun test

Output:

text
bun test v1.2.14

math.test.ts:
✓ add() > adds two positive numbers [0.43ms]
✓ add() > supports snapshots [0.71ms]

 2 pass
 0 fail
 1 snapshot
 4 expect() calls
Ran 2 tests across 1 files. [12.00ms]

Useful test flags

FlagPurpose
--watchRe-run on file change.
--coveragePrint line/branch coverage; writes lcov when paired with --coverage-reporter=lcov.
--timeout <ms>Per-test timeout (default 5000).
--bail [n]Stop after n failures (default 1).
-t "name pattern"Run only matching tests by name.
--update-snapshotsRefresh .snap files.
--reporter <name>junit for CI, tap for piping.

Coverage report at 80% threshold:

bash
bun test --coverage --coverage-reporter=text --coverage-threshold=0.8

Output:

text
-------------|---------|---------|-------------------
File         | % Funcs | % Lines | Uncovered Line #s
-------------|---------|---------|-------------------
All files    |  100.00 |   95.45 |
 math.ts     |  100.00 |  100.00 |
 user.ts     |  100.00 |   91.67 | 24
-------------|---------|---------|-------------------
✓ Coverage thresholds met

Built-in APIs (Bun.*)

Bun exposes a handful of high-performance APIs under the Bun.* namespace. They're optional — every Node API (fs, path, http, etc.) still works — but the Bun variants are significantly faster because they're implemented in Zig and skip the Node compatibility shim.

Bun.serve — HTTP server

Bun.serve is a built-in HTTP/HTTPS/WebSocket server roughly 4× faster than Node's http.createServer. It accepts a fetch handler that takes a standard Request and returns a Response — the same shape as Cloudflare Workers and Deno.

typescript
// server.ts
Bun.serve({
  port: 3000,
  fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === "/health") {
      return Response.json({ status: "ok" });
    }
    return new Response("Hello, Alice Dev!");
  },
});

console.log("Listening on http://localhost:3000");
bash
bun --hot server.ts

Output:

text
Listening on http://localhost:3000

WebSocket upgrade with Bun.serve:

typescript
Bun.serve({
  port: 3000,
  fetch(req, server) {
    if (server.upgrade(req)) return; // upgraded, no response needed
    return new Response("HTTP root");
  },
  websocket: {
    open(ws) { ws.send("welcome"); },
    message(ws, msg) { ws.send(`echo: ${msg}`); },
  },
});
bash
bun server.ts

Output: (none — exits 0 on success)

Bun.file — lazy file handle

Bun.file(path) returns a lazy BunFile that doesn't touch disk until you await it as text, JSON, ArrayBuffer, or stream. It implements the standard Blob interface so you can pass it directly to fetch() bodies or new Response().

typescript
const file = Bun.file("./package.json");

const text = await file.text();
const json = await file.json();
const buf = await file.arrayBuffer();
const exists = await file.exists();

console.log({ size: file.size, type: file.type, exists });
bash
bun file.ts

Output:

text
{
  size: 487,
  type: "application/json;charset=utf-8",
  exists: true
}

Bun.write — fast file writes

Bun.write accepts a destination (path, BunFile, Response) and a payload (string, Buffer, Blob, Response). Single call, no manual fs.writeFile wiring.

typescript
// Write a string
await Bun.write("./out.txt", "Hello Alice Dev!");

// Stream an HTTP response straight to disk
await Bun.write("./logo.png", await fetch("https://example.com/logo.png"));

// Copy a file
await Bun.write(Bun.file("./copy.txt"), Bun.file("./original.txt"));
bash
bun write.ts

Output: (none — exits 0 on success)

Bun.password — bcrypt / argon2

Native password hashing without bcrypt or argon2 npm packages. Defaults to argon2id; bcrypt is available via algorithm: "bcrypt".

typescript
const hash = await Bun.password.hash("hunter2");
const ok = await Bun.password.verify("hunter2", hash);
console.log({ hash, ok });
bash
bun password.ts

Output:

text
{
  hash: "$argon2id$v=19$m=65536,t=2,p=1$rJrPL5p9...",
  ok: true,
}

Bun.spawn — subprocess

A streaming subprocess API similar to Deno's Deno.Command. Returns a Subprocess with stdout/stderr as ReadableStreams and an awaitable exited promise.

typescript
const proc = Bun.spawn(["git", "rev-parse", "HEAD"], {
  cwd: "/home/alice/myproject",
});

const sha = (await new Response(proc.stdout).text()).trim();
console.log("HEAD:", sha);
bash
bun spawn.ts

Output:

text
HEAD: 9f8e7d6c5b4a3210fedcba9876543210ff112233

Workspaces (monorepos)

Bun reads the workspaces field in the root package.json (same shape as npm/pnpm). All workspace deps get hoisted into the root node_modules, and intra-workspace deps are symlinked automatically.

json
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*", "apps/*"]
}
bash
# Install every workspace from the root
bun install

# Run a script in a specific workspace
bun run --filter='./apps/web' build
bun run --filter='@alicedev/*' test

# Add a dep to a single workspace
bun add zod --filter='./apps/web'

Output (bun run --filter='./apps/web' build):

text
@alicedev/web build $ vite build
[12:14:02] vite v5.2.10 building for production...
✓ built in 487ms

Node.js compatibility

Bun implements roughly 90% of Node's stdlib API. node:fs, node:http, node:crypto, node:buffer, process, Buffer, require, module.exports, __dirname, __filename — all work as expected.

typescript
// All of this runs identically on Node and Bun
import { readFile } from "node:fs/promises";
import http from "node:http";

const data = await readFile("./config.json", "utf8");
http.createServer((req, res) => {
  res.end("Hello from " + (typeof Bun !== "undefined" ? "Bun" : "Node"));
}).listen(3000);
bash
bun server.ts   # runs
node server.ts  # also runs

Output: (none — exits 0 on success)

Check runtime feature parity at runtime:

typescript
console.log(typeof Bun);          // "object" on Bun, "undefined" on Node
console.log(process.versions);    // includes "bun" key on Bun
bash
bun -e 'console.log(process.versions)'

Output:

text
{
  node: "22.10.0",
  bun: "1.2.14",
  webkit: "619.1.21.110.0",
  ...
}

The compatibility table at bun.sh/docs/runtime/nodejs-apis tracks which APIs are fully supported, partially supported, or stubbed.

bun create — project scaffolding

bun create <template> clones a starter into a new directory and runs bun install. Templates can be official names, GitHub repos, or local paths.

bash
# Official templates
bun create react ./my-react-app
bun create elysia ./my-api          # Elysia HTTP framework
bun create hono ./my-hono-app

# Any GitHub repo with a package.json at the root
bun create github.com/alicedev/my-template ./my-project

# Local template
bun create ./templates/api ./new-service

Output (bun create react ./my-react-app):

text
bun create v1.2.14

✓ Cloning into ./my-react-app
✓ bun install
✓ git init

Done! Run "cd my-react-app && bun run dev"

bun init is the lighter version — interactive prompts inside the current directory, no clone:

bash
bun init

Output:

text
package name (my-project):
entry point (index.ts):

Done! A package.json file was saved in the current directory.
 + index.ts
 + .gitignore
 + tsconfig.json (for editor auto-complete)
 + README.md

To get started, run:
  bun run index.ts

Common pitfalls

  1. Don't mix bun.lock and package-lock.json — pick one tool per repo. Bun reads npm lockfiles for compatibility but writes its own; committing both confuses CI.
  2. node_modules is hoisted differently — packages relying on a specific install layout (most do not, but old peerDependencies plugins can) may misbehave. Add the offending package to trustedDependencies or fall back to npm for that project.
  3. Native modules with N-API — most work, but some that link to libuv internals do not. Run bun add then check for build errors before assuming a green install.
  4. bun --hot keeps state — module-level state (caches, in-memory DBs) persists across reloads. Use bun --watch if you need a clean process each time.
  5. bun build --compile produces large binaries — typically 50–90 MB because the runtime is included. That's the trade-off for a zero-dependency executable; use node/bun runtime + plain bundle for size-constrained distribution.
  6. Tests under bun test use bun:test, not Vitest — they're API-compatible but you can't pass Vitest config flags (--reporter=verbose works, --ui does not). Add a separate bun vitest script if you need Vitest features.
  7. bun install --production strips devDependencies — useful in Dockerfiles but breaks if your build step (Vite, esbuild) is in devDependencies. Build first, then re-install with --production.
  8. Windows support is newer — most things work, but some Bun APIs (Bun.spawn with PTY, certain socket options) lag behind Linux/macOS. Check the release notes if a feature seems missing.

Real-world recipes

Single-binary HTTP server with TypeScript hot-reload

A pattern this project uses for prototyping: TypeScript file, no compile step, --hot reloads the handler while keeping the listening socket open.

typescript
// server.ts
const PORT = Number(process.env.PORT) || 3000;

Bun.serve({
  port: PORT,
  fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === "/health") {
      return Response.json({ status: "ok", env: process.env.NODE_ENV });
    }
    if (url.pathname.startsWith("/echo/")) {
      return new Response(url.pathname.slice(6));
    }
    return new Response("Not Found", { status: 404 });
  },
});

console.log(`Listening on http://localhost:${PORT}`);
bash
bun --hot server.ts

Output:

text
Listening on http://localhost:3000

Hit it from another shell:

bash
curl -s http://localhost:3000/health

Output:

text
{"status":"ok","env":"development"}

Replace npm install in CI with bun install

In a GitHub Actions workflow, swap npm ci for bun install --frozen-lockfile and shave 60–90% off install time.

yaml
# .github/workflows/ci.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: 1.2.14
      - run: bun install --frozen-lockfile
      - run: bun test --coverage
bash
bun install --frozen-lockfile

Output:

text
bun install v1.2.14

 287 packages installed [1.42s]

Compile a CLI to a portable executable

Bundle a TypeScript CLI plus the Bun runtime into a single ~60 MB binary you can ship to any matching OS without requiring Bun (or Node) on the target.

typescript
// cli.ts
#!/usr/bin/env bun
const [name = "world"] = Bun.argv.slice(2);
console.log(`Hello, ${name}!`);
bash
bun build ./cli.ts --compile --outfile=hello
./hello "Alice Dev"

Output:

text
   ./hello  61.84 MB

[1.4s] bundle 1 module
Hello, Alice Dev!

Cross-compile for a different platform from the same machine:

bash
bun build ./cli.ts --compile --target=bun-linux-arm64 --outfile=hello-linux-arm64
bun build ./cli.ts --compile --target=bun-windows-x64 --outfile=hello.exe

Output: (none — exits 0 on success)

Run a Vite project under Bun

Vite spawns Node by default. bun --bun vite dev forces every child process to be Bun too, often shaving 30–50% off cold-start time.

bash
bun add -d vite @vitejs/plugin-react
bun --bun vite dev --port 5173

Output:

text
  VITE v5.2.10  ready in 187 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

Migrate an existing npm project to Bun

Drop the npm-managed node_modules, let Bun re-resolve from package.json, and run the same scripts. No config changes — Bun reads package.json natively.

bash
rm -rf node_modules package-lock.json
bun install
bun run dev

Output:

text
bun install v1.2.14

 287 packages installed [1.21s]

$ vite dev
  VITE v5.2.10  ready in 198 ms

If a single package misbehaves, mark it as trusted so postinstall scripts run:

json
{
  "trustedDependencies": ["sharp", "@swc/core"]
}
bash
bun install

Output: (none — exits 0 on success)