From skills
Use when writing tests, scripts, servers, or build tooling in a project that uses Bun as its runtime, package manager, or test runner. Covers the Bun-native APIs (Bun.serve, Bun.file, bun test, bun:sqlite, $ shell), package management, config via bunfig.toml, single-file executables, and Node-compat and portability tradeoffs.
How this skill is triggered — by the user, by Claude, or both
Slash command
/skills:bun-expertiseThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
- A repo has `bun.lock`, a `bunfig.toml`, or a `package.json` with `"engines": { "bun": ... }`.
bun.lock, a bunfig.toml, or a package.json with "engines": { "bun": ... }.tsx, ts-node, vitest, dotenv, nodemon) and unsure whether Bun already covers it.Targets Bun 1.2+ (text bun.lock, Bun.serve routes, bun:sqlite). Node-compat coverage and newer APIs shift between minors — check the live docs. Canonical entry points:
WebFetch the Node-compat page when unsure whether a node:* API works yet — it's an ongoing effort, not complete.
The concepts that decide when to reach for a Bun primitive:
.ts/.tsx on the fly and honours tsconfig.json for paths/JSX/target — no tsx/ts-node/transpile step. It does not type-check at runtime; still run tsc --noEmit in CI. See [[typescript-strict-expertise]].fetch, Request/Response, URL, WebSocket, ReadableStream natively. Server and HTTP code built on these stays portable; Bun.*/bun:* APIs do not (see Portability).process, fs, path, http, crypto, Buffer, and most node:* work. Reach for Node APIs when the Bun equivalent doesn't exist or the code must also run on Node.Bun.serve, Bun.file, bun:sqlite, $ are faster and ergonomic but lock code to Bun. Use them freely in scripts/tests; keep published library and cross-runtime code standards-based.In a Bun project, default to the Bun primitive rather than the Node ecosystem equivalent:
| Need | Don't reach for | Use |
|---|---|---|
| Run TypeScript | tsx, ts-node, node --loader | bun run script.ts (runs TS directly) |
| Install deps | npm install, pnpm install | bun install (uses bun.lock) |
| Run a package binary | npx | bunx (or bun x) |
| Test runner | vitest, jest | bun test |
| Watch/restart | nodemon, node --watch | bun --watch / bun --hot |
| Bundle | esbuild, webpack | bun build |
| Read env files | dotenv | Bun reads .env automatically |
| HTTP server | express, fastify for trivial cases | Bun.serve({ ... }) |
| Read/write files | fs.promises.readFile/writeFile | Bun.file(path) / Bun.write(path, data) |
| Spawn processes | child_process.spawn | Bun.spawn([...]) / $\...`` shell |
| Embedded SQL | better-sqlite3 | import { Database } from "bun:sqlite" |
| Hash a password | bcrypt | Bun.password.hash/verify |
bun run scripts/validate.ts # runs TS directly, no transpile step
bun script.ts # short form
bun --watch run server.ts # restart on file change
bun --hot run server.ts # hot-reload without dropping state
package.json scripts run under Bun when invoked with bun run <name>. bun <name> also works if it doesn't collide with a built-in command.
bun install # install from package.json + bun.lock
bun add zod # add a dependency
bun add -d @types/bun # dev dependency
bun add -g <tool> # global
bun update # update within ranges
bun outdated # show what's behind
bun.lock (text, since Bun 1.2; older repos may have binary bun.lockb)."workspaces": ["packages/*"] in the root package.json; bun install links them.bunx <pkg> runs a package binary without installing it (like npx).bun testimport { test, expect, describe, mock, beforeEach } from "bun:test";
describe("foo", () => {
test("works", () => {
expect(1 + 1).toBe(2);
});
});
"bun:test", not "vitest". Jest-compatible expect matchers.*.test.ts, *.spec.ts, *.test.tsx, etc.bun test. Filter by name with bun test -t "pattern", or pass a path. Watch with --watch, coverage with --coverage.mock(fn) is the vi.fn() equivalent; mock.module("./x", () => ({...})) stubs a module. Lifecycle hooks beforeEach/afterAll come from "bun:test" too.const text = await Bun.file("config.json").text();
const json = await Bun.file("config.json").json();
const buf = await Bun.file("blob.bin").arrayBuffer();
await Bun.write("out.txt", "hello");
await Bun.write("out.json", JSON.stringify({ foo: 1 }));
await Bun.write("copy.bin", Bun.file("in.bin")); // streamed copy
Bun.file() is lazy — it doesn't touch disk until you read it. Check existence with await Bun.file(path).exists().
Bun.serve({
port: 3000,
routes: {
"/": new Response("hi"),
"/users/:id": (req) => Response.json({ id: req.params.id }),
},
fetch(req) {
return new Response("not found", { status: 404 });
},
});
Request/Response/URL — no framework needed for simple servers. routes (Bun 1.2+) gives path-param routing; fetch is the fallback.websocket handler and call server.upgrade(req) inside fetch.bun --hot run server.ts.import { $ } from "bun";
const branch = (await $`git rev-parse --abbrev-ref HEAD`.text()).trim();
await $`mkdir -p dist && cp ${file} dist/`; // interpolation auto-escapes
$ runs a cross-platform shell (works on Windows without WSL). Interpolated values are escaped, so $\ls ${userInput}`is injection-safe. Use.quiet()to suppress output,.nothrow()` to not throw on non-zero exit.
import { Database } from "bun:sqlite";
const db = new Database("app.db");
const rows = db.query("SELECT * FROM users WHERE age > ?").all(18);
Synchronous, built in — no native module to compile. :memory: for an in-memory DB.
Bun reads .env, .env.local, .env.{NODE_ENV} automatically — no dotenv.config(). Access via process.env.FOO or Bun.env.FOO. Precedence (most → least): .env.local > .env.{NODE_ENV} > .env.
tsconfig setup needed to run — Bun resolves and transforms TS on the fly and honours tsconfig.json for paths, JSX, and target.tsc --noEmit (or bun tsc) in CI and rely on your editor.@types/bun (dev) so Bun.*, bun:*, and $ are typed.Project- and user-level settings live in bunfig.toml (next to package.json). Common keys: a [test] block with preload for setup files and coverageThreshold; [install] registry/scopes; a global preload for instrumentation. Reach for it when a flag needs to apply to every bun invocation.
bun build ./cli.ts --compile --outfile mycli
--compile bundles your code plus the Bun runtime into a standalone binary — handy for shipping CLIs without a Node install. Add --minify and --target=bun-linux-x64 to cross-compile.
tsx, ts-node, or a transpile step solely to run TS — Bun runs TS natively.dotenv — it does nothing useful in Bun.nodemon — use bun --watch / bun --hot.vitest/jest for new tests in a Bun project — bun test is faster and built-in. Use Vitest only when you need a feature Bun's runner lacks."vitest" and expect bun test to pick it up — the runner reads "bun:test".npm install/pnpm install into a Bun project — the lockfile is bun.lock, and switching managers thrashes deps.Bun.serve()/bun:sqlite/$ in code that must also run on Node — see Portability.Bun-specific APIs (Bun.*, $, "bun:..." imports) make code non-portable. Acceptable when:
Bun.file streaming, bun:sqlite vs a native module).Avoid Bun-specific APIs in:
npm install and expect it to work.A pragmatic split: scripts and the test runner use Bun APIs freely; portable library/application code stays standards-based. For servers that must run on both, use a standards-based framework (Hono, Elysia) over raw Bun.serve.
bun build as a bundler/target alongside esbuild and friends.strict: true in tsconfig costs nothing.@effect/platform-bun provides BunFileSystem / BunRuntime, the Bun counterparts to @effect/platform-node.npx claudepluginhub thomasfosterau/skills --plugin skillsGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.