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.
# 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.
bun --version
Output:
1.2.14
Upgrade in place — Bun manages its own binary and keeps the previous version for rollback.
bun upgrade
Output:
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.
bun <subcommand|file> [args] [--flag value]
Output: (none — exits 0 on success)
Essential commands
| Command | Purpose |
|---|---|
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 test | Run the built-in test runner. |
bun create <template> | Scaffold a new project from a template. |
bun init | Interactive 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 upgrade | Self-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.
// 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}!`);
bun hello.ts
Output:
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).
# 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:
[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).
// args.ts
console.log(Bun.argv.slice(2));
bun args.ts foo bar --flag
Output:
[ "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.
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.
# 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):
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.
bun install --frozen-lockfile
Output: (none — exits 0 on success)
Inspect the installed tree:
bun pm ls
Output:
/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.
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):
/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.
# 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.
# 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):
./dist/main.js 3.42 KB (entry point)
./dist/chunk-Q5J8.js 1.18 KB
[12ms] bundle 2 modules
Bundler options reference
| Flag | Purpose |
|---|---|
--outdir <dir> | Directory for emitted files (use with code splitting). |
--outfile <path> | Single-file output (mutually exclusive with --outdir). |
--target | browser (default), node, or bun. |
--format | esm, cjs, or iife. |
--minify | Minify identifiers, whitespace, and syntax. |
--sourcemap | none, linked, external, or inline. |
--splitting | Emit shared chunks (ESM only). |
--external <pkg> | Mark a dep as external (don't bundle). |
--define KEY=value | Substitute identifiers at compile time. |
--compile | Bake 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.
// 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();
});
});
bun test
Output:
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
| Flag | Purpose |
|---|---|
--watch | Re-run on file change. |
--coverage | Print 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-snapshots | Refresh .snap files. |
--reporter <name> | junit for CI, tap for piping. |
Coverage report at 80% threshold:
bun test --coverage --coverage-reporter=text --coverage-threshold=0.8
Output:
-------------|---------|---------|-------------------
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.
// 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");
bun --hot server.ts
Output:
Listening on http://localhost:3000
WebSocket upgrade with Bun.serve:
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}`); },
},
});
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().
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 });
bun file.ts
Output:
{
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.
// 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"));
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".
const hash = await Bun.password.hash("hunter2");
const ok = await Bun.password.verify("hunter2", hash);
console.log({ hash, ok });
bun password.ts
Output:
{
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.
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);
bun spawn.ts
Output:
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.
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*", "apps/*"]
}
# 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):
@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.
// 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);
bun server.ts # runs
node server.ts # also runs
Output: (none — exits 0 on success)
Check runtime feature parity at runtime:
console.log(typeof Bun); // "object" on Bun, "undefined" on Node
console.log(process.versions); // includes "bun" key on Bun
bun -e 'console.log(process.versions)'
Output:
{
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.
# 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):
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:
bun init
Output:
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
- Don't mix
bun.lockandpackage-lock.json— pick one tool per repo. Bun reads npm lockfiles for compatibility but writes its own; committing both confuses CI. node_modulesis hoisted differently — packages relying on a specific install layout (most do not, but oldpeerDependenciesplugins can) may misbehave. Add the offending package totrustedDependenciesor fall back to npm for that project.- Native modules with N-API — most work, but some that link to libuv internals do not. Run
bun addthen check for build errors before assuming a green install. bun --hotkeeps state — module-level state (caches, in-memory DBs) persists across reloads. Usebun --watchif you need a clean process each time.bun build --compileproduces large binaries — typically 50–90 MB because the runtime is included. That's the trade-off for a zero-dependency executable; usenode/bunruntime + plain bundle for size-constrained distribution.- Tests under
bun testusebun:test, not Vitest — they're API-compatible but you can't pass Vitest config flags (--reporter=verboseworks,--uidoes not). Add a separatebun vitestscript if you need Vitest features. bun install --productionstrips devDependencies — useful in Dockerfiles but breaks if your build step (Vite, esbuild) is in devDependencies. Build first, then re-install with--production.- Windows support is newer — most things work, but some Bun APIs (
Bun.spawnwith 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.
// 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}`);
bun --hot server.ts
Output:
Listening on http://localhost:3000
Hit it from another shell:
curl -s http://localhost:3000/health
Output:
{"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.
# .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
bun install --frozen-lockfile
Output:
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.
// cli.ts
#!/usr/bin/env bun
const [name = "world"] = Bun.argv.slice(2);
console.log(`Hello, ${name}!`);
bun build ./cli.ts --compile --outfile=hello
./hello "Alice Dev"
Output:
./hello 61.84 MB
[1.4s] bundle 1 module
Hello, Alice Dev!
Cross-compile for a different platform from the same machine:
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.
bun add -d vite @vitejs/plugin-react
bun --bun vite dev --port 5173
Output:
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.
rm -rf node_modules package-lock.json
bun install
bun run dev
Output:
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:
{
"trustedDependencies": ["sharp", "@swc/core"]
}
bun install
Output: (none — exits 0 on success)