bun
Write and review Bun: the Bun-native API surface against its `node:` equivalents, the HTTP and WebSocket server, file and process I/O, the shell, the data clients, `bun:test`, the bundler and compiled
SKILL.mdplugins/javascript/skills/bun/SKILL.md
GitHubBun's value is that the runtime, the toolchain, and the batteries are one binary. Two biases decide most calls:
- Reach for what already ships. A dependency earns its place by doing what
Bun.*,bun:*, and the bundled toolchain do not. Adding one that duplicates a built-in is a defect. - Write
node:only where Bun has no native equivalent, or where the code must also run under Node. Portability is a requirement to state, not a habit to keep.
Release Version
Bun ships one stable line and no long-lived support branches, so bun upgrade moves a machine to whatever release is
newest. Pin the version in CI (oven-sh/setup-bun, bun-version) so a developer machine and a runner agree, and state
the release a feature needs in the sentence that uses it. Never write "the latest version supports X".
The rules below assume 1.3.0 or later unless a version anchor says otherwise. Floors for the features these rules reference:
- 1.3.0 —
Bun.sqlfor MySQL, MariaDB, and SQLite;Bun.redis;Bun.secrets;Bun.YAML; catalogs;test.concurrent,test.serial, and--concurrent;--randomize;expectTypeOf - 1.3.2 — the isolated linker as the default for new monorepos;
nativeDependencies;ignoreScripts - 1.3.3 —
test(..., { retry }) - 1.3.4 —
jest.useFakeTimers();URLPattern;--compilestops auto-loadingtsconfig.jsonandpackage.json - 1.3.5 —
Bun.Terminal;Bun.stringWidth;bun:bundlefeature flags;trustedDependencieslimited to npm sources - 1.3.6 —
Bun.JSONC;Bun.Archive;Bun.build({ files });metafile: true - 1.3.7 —
Bun.JSON5;Bun.JSONL;Bun.wrapAnsi - 1.3.8 —
Bun.markdown;--metafile-md - 1.3.9 —
bun run --parallel; bytecode for ES modules - 1.3.10 —
optimizeImports; standard TC39 decorators;--compile --target=browserfor a single HTML file - 1.3.11 —
Bun.cron();Bun.sliceAnsi - 1.3.12 —
Bun.WebView - 1.3.13 —
bun test --parallel,--isolate,--shard,--changed;Rangeand conditional requests inBun.serve - 1.3.14 —
Bun.Image; the global virtual store; HTTP/3 inBun.serve; HTTP/2 and HTTP/3 infetch() - 1.4.0 — Node 26 compatibility;
Bun.XML;Bun.isStandaloneExecutable;Bun.spawn({ cgroup }); directory routes;bun audit fix,bun dedupe,bun prune,bun pm diff,bun pm licenses;bun test --timings;--asset;lockfileVersion: 2
Prefer the Built-in
Reach for the built-in, and drop the package it replaces from package.json.
- HTTP server —
Bun.serve()overhttp.createServer(), Express, or Fastify - Files —
Bun.file()andBun.write()overnode:fsread and write helpers - Directories —
node:fs. There is noBun.*equivalent formkdirorreaddir - Globbing —
Bun.Globoverglobandfast-glob - Shelling out —
Bun.$overchild_process.exec,zx,execa,cross-env, andrimraf - Processes —
Bun.spawnandBun.spawnSyncoverchild_process.spawn - Pseudo-terminals —
Bun.Terminalovernode-pty(1.3.5) - SQLite —
bun:sqliteoverbetter-sqlite3 - Postgres, MySQL, MariaDB —
Bun.sqloverpgandmysql2(1.3.0) - Redis and Valkey —
Bun.redisoverioredis(1.3.0) - S3-compatible storage —
Bun.S3Clientover@aws-sdk/client-s3 - Password hashing —
Bun.passwordoverbcryptandargon2 - Testing —
bun:testover Jest - Bundling —
Bun.build()andbun buildover webpack, Rollup, and esbuild - Images —
Bun.Imageoversharp(1.3.14) - Headless browsing —
Bun.WebViewover Puppeteer (1.3.12) - Markdown —
Bun.markdownovermarked(1.3.8) - Scheduling —
Bun.cron()overnode-cron(1.3.11) - Running scripts concurrently —
bun run --parallelovernpm-run-allandconcurrently(1.3.9) - Config formats —
Bun.TOML,Bun.YAML,Bun.JSON5(1.3.7),Bun.JSONC(1.3.6),Bun.XML(1.4.0) over@iarna/toml,js-yaml,json5,jsonc-parser, andfast-xml-parser - Newline-delimited JSON —
Bun.JSONLoverndjson(1.3.7) - Tarballs —
Bun.Archiveovertar(1.3.6) - Terminal string width —
Bun.stringWidth(1.3.5),Bun.wrapAnsi(1.3.7),Bun.sliceAnsi(1.3.11) overstring-width,wrap-ansi,slice-ansi, andcli-truncate - Route patterns —
URLPatternoverpath-to-regexp(1.3.4) - Credential storage —
Bun.secretsover a plaintext dotfile, for local development tools - Sleeping, hashing, comparison, compression —
Bun.sleep,Bun.hash,Bun.CryptoHasher,Bun.deepEquals,Bun.gzipSync,Bun.zstdCompressSync
Two boundaries hold in the other direction. Web Streams are the native currency — Bun.file().stream(),
Response.body, and proc.stdout are all ReadableStream, so node:stream is for code shaped around Node, not the
default. crypto.randomUUID() is the standard v4 UUID; Bun.randomUUIDv7() is the time-ordered variant, not a
replacement for it.
HTTP Server
- Declare routes in the
routesobject, and keepfetchfor genuinely unmatched requests. Precedence is exact, then parameterized, then wildcard, then the global catch-all — not source order. - Never deploy without either an
errorhandler or an explicitdevelopment: false.developmentdefaults toprocess.env.NODE_ENV !== "production", so a server whose environment does not setNODE_ENV=productionrenders Bun's contextual error page to any client that can trigger a throw. On 1.3.14 that page is a ~67 KB HTML overlay carrying the thrown error's payload in an embedded__bunfallbackscript. - An
errorhandler decides the response body in both modes and replaces the error page whendevelopmentis on. Without one,development: falseanswers 500 with the bodySomething went wrong!(measured on 1.3.14). The stack trace reaches stderr either way, so a missing handler costs you the response, never the log. idleTimeoutis 10 seconds and it covers a request whose handler has not yet written a byte. A slow handler, a long poll, or a server-sent-events stream is killed mid-response and the client sees a connection reset. RaiseidleTimeout, or callserver.timeout(req, 0)for the one request that needs it.- Return a
Responseinstance as the route value for anything fixed — health checks, redirects, constant JSON. Bun dispatches it with no allocation and caches it for the server's lifetime, soserver.reload()is the only way to change it. req.cookieswrites are applied to the response only underroutes. Inside a barefetchhandler nothing is tracked and you buildSet-Cookieyourself.server.upgrade(req)must be followed by returningundefined, never aResponse.- Treat the return of
send()andpublish()as a status, not a byte count.-1is backpressure and0is dropped; from 1.4.0publish()also returns0when the topic has no subscribers. - WebSocket handlers are declared once per server, in the
websocketobject, not per socket. Typews.datathrough the handler object'sdataproperty. Response.error()and any status outside 100–999 route toerror()and answer 500 (1.4.0). Earlier releases wrote an invalid status line.
Read [${CLAUDE_SKILL_DIR}/references/http-server.md] when serving files or a directory, wiring WebSocket pub/sub,
tuning server lifecycle or timeouts, or enabling HTTP/2 or HTTP/3 — it carries the five route value shapes and their
caching and 404 behavior, the directory-route path rules, the full websocket option set with its defaults, and the
stop, reload, and closeIdleConnections semantics.
Files
Bun.file(path)is lazy. Constructing it touches no disk;sizeis0andexists()isfalsefor a missing path rather than throwing.Bun.write(dest, data)takes anything: string,Blob,BunFile,ArrayBuffer,TypedArray, orResponse. It picks the fastest syscall for the pair, so copying a file isBun.write(Bun.file(dst), Bun.file(src)), not a manual read-then-write.file.writer()returns aFileSinkfor incremental writes, and the process stays alive until.end()is called..unref()opts out.Bun.stdin,Bun.stdout, andBun.stderrareBunFiles, so streaming a file to stdout is oneBun.writecall.
Shell and Processes
- Interpolated values in
Bun.$are single literal strings, so command injection is closed by construction. Two holes remain and neither is Bun's to close: spawningbash -c "...${input}"hands control to a real shell, and an external command may read attacker-supplied text as its own flag (--upload-pack=). Validate arguments you pass on. - Only glob patterns written in the template expand (1.4.0). A
*arriving through${...}, a shell variable, command substitution, or quoted text is literal, and?,[...], and a leading!are literal everywhere. - A non-zero exit throws
ShellError. Use.nothrow()where you checkexitCodeyourself. Bun.$is not/bin/sh. It is an in-process interpreter, so.shfiles run identically on Windows and the builtin set is fixed.stderrdefaults to"inherit"inBun.spawn, soproc.stderrisundefined. Passstderr: "pipe"to read it.stdoutdefaults to"pipe"andstdintonull.- The parent process does not exit while a child is alive. Call
proc.unref()for a child that must not hold it open. - Set
serialization: "json"for IPC with a Node process. The default"advanced"is JavaScriptCore's format and Node cannot read it.
Read [${CLAUDE_SKILL_DIR}/references/shell-and-processes.md] when redirecting shell I/O into JavaScript objects,
driving an interactive program through a PTY, applying cgroup limits, or spawning a Worker — it carries the
redirection and builtin tables, the stdio option matrix, the Bun.Terminal platform differences, and the worker
options.
Testing
- Import from
bun:test, never fromjestorvitest.jest.*andvi.*exist insidebun:testfor ported suites. mock.module()is not hoisted. Unlikejest.mock, it runs where it is written, so a module imported above it has already been evaluated and its side effects have already happened. Put the override in a--preloadscript when the original must never run.- Pass the dependency in before reaching for
mock.module(). A fake passed as a parameter or a constructor argument replaces one collaborator for one test. A module override reaches every importer, even one that imported the module before the override, andmock.restore()does not undo it. - Pick the right global reset.
mock.clearAllMocks()clears history and keeps implementations;jest.resetAllMocks()also drops them (1.4.0 — before, it behaved likeclearAllMocks);mock.restore()restores spied originals and undoes nomock.module()override. expectTypeOfis a no-op at runtime. A greenbun testproves nothing about types — run the project's type-check command separately.bun testsetsNODE_ENV=testunless it is already set, and runs in UTC.process.env.TZstays unset — the runtime zone is UTC, not an environment variable a preload can read.- An unhandled rejection between tests fails the run. When it happens while a file loads, none of that file's tests run at all.
- Three concurrency knobs are independent:
--parallelspreads files across processes,--concurrentandtest.concurrentoverlap async tests inside one file,--shard=i/nsplits files across machines (--paralleland--shardfrom 1.3.13).--parallelimplies--isolate. - Reach for
--parallel --no-isolateon a suite of many small files sharing a large import graph. Isolation re-evaluates every import per file, and on that shape plainbun testbeats isolated workers. - Key per-worker resources off
BUN_TEST_WORKER_ID(orJEST_WORKER_ID) so parallel workers do not share a database or a port. - The default per-test timeout is 5000 ms.
Read [${CLAUDE_SKILL_DIR}/references/testing.md] when writing the first test in a project, configuring CI sharding,
reaching for a matcher or a snapshot form, or debugging a mock that stopped returning values — it carries the discovery
patterns, the matcher inventory, the mock-reset semantics in full, the --timings sharding workflow, and the
bunfig.toml test keys.
Package Manager
trustedDependenciesreplaces Bun's built-in allow list; it never extends it. Adding one package silently disablespostinstallfor every package the built-in list covered, so re-list the ones you still need.[]opts out of lifecycle scripts entirely.- The built-in list applies only to npm-registry sources (1.3.5). A
file:,link:,git:, orgithub:dependency namedesbuildinherits nothing from the realesbuild. - The linker default comes from the lockfile's
configVersion, not from a global setting. A new monorepo gets the isolated linker (1.3.2); a new single-package project and every project whose lockfile predates 1.3.2 get the hoisted one. Two machines therefore lay outnode_modulesdifferently unlessbun.lockis committed. - The isolated linker still permits phantom dependencies by default. Set
[install] hoist = falseto stopnode_modules/.bun/node_modulesfrom being created. - Run
bun ciin CI, notbun install. Bun does not turn on the frozen lockfile automatically;bun ciisbun install --frozen-lockfile. --productionimplies--frozen-lockfileand skipsdevDependencies, but removes nothing already installed.bun prune --production(1.4.0) is what shrinks a build image.
Read [${CLAUDE_SKILL_DIR}/references/package-manager.md] when configuring a monorepo, choosing a linker, hardening a
supply chain, or diagnosing an install that differs between machines — it carries the configVersion table, the
trustedDependencies modes, workspaces and catalogs, the --filter grammar, minimumReleaseAge and the security
scanner, and the bunfig.toml install keys.
Building and Compiling
Bun.build()rejects with anAggregateErroron failure.throwdefaults totrue, soresult.successis meaningful only when you passthrow: false. Checkingsuccesson a default build reads a value that never arrives.targetdefaults to"browser". Settarget: "bun"for server code, which also emits the// @bunpragma so the runtime skips re-transpiling.- Ship
--compile --minify --sourcemap --bytecodefor a production binary. Bytecode needstarget: "bun", and for ES modules it needs--compileas well (1.3.9). - A compiled binary does not auto-load
tsconfig.jsonorpackage.json(1.3.4), but it does auto-load.envandbunfig.toml. Pass--no-compile-autoload-dotenvand--no-compile-autoload-bunfigwhere a deployment must not read files from its working directory. - Cross-compile with
--target bun-<os>-<arch>[-musl]. From 1.4.0 x64 ships only the baseline build and the-march=haswellbuild is gone, so-baselineand-modernresolve to the same binary and neither is a choice. - A macro's return value must be serializable and its arguments statically known. Code inside
node_modulescannot invoke a macro, though your own code may import one from a package. Bun.markdown.html()does not sanitize. Raw HTML, event-handler attributes, andjavascript:hrefs pass through verbatim, so untrusted Markdown needs a sanitizer before it reaches a browser.
Read [${CLAUDE_SKILL_DIR}/references/build-and-compile.md] when configuring a bundle, embedding assets in an
executable, writing a plugin, or reaching for a macro — it carries the full Bun.build option set, code-splitting and
optimizeImports behavior, the cross-compilation targets, the embedded-file and /$bunfs rules, the plugin hooks, and
the macro serialization constraints.
Data Clients
- Open
bun:sqlitewith{ strict: true }. Without it a missing bind parameter is not an error — a typo binds nothing and the query returns a wrong result silently. db.query()caches the compiled statement (20 most recent);db.prepare()does not. Useprepare()for SQL generated at runtime so one-off queries do not evict the statements that repeat.- Enable WAL with
PRAGMA journal_mode = WALfor anything with concurrent readers. - Set
safeIntegers: truewhen a column can exceed 2^53. The default returnsnumberand rounds silently. - Every interpolation in a
Bun.sqltemplate is a parameter. Use thesql()helper for identifiers, column subsets, and value lists;sql.unsafeescapes nothing. Bun.sqlqueries are lazy — they start onawaitor.execute(), and.cancel()stops a running one.- Release a reserved connection.
sql.reserve()takes one out of the pool untilrelease(), or useusing. - Set
prepare: falsebehind PgBouncer in transaction mode.
Read [${CLAUDE_SKILL_DIR}/references/data-clients.md] when choosing a client, closing a SQLite database cleanly,
building dynamic SQL, or configuring pooling and TLS — it carries the bun:sqlite close and WAL-sidecar semantics, the
Bun.sql fragment helpers and query modes, and the Bun.redis and Bun.S3Client configuration.
Configuration and Environment
bunfig.tomlconfigures the runtime,bun test,bun install, andbun run. A CLI flag overrides it; a project'sbunfig.tomloverrides.npmrcfor the same key (1.4.0)..envfiles load automatically, in order:.env, then.env.{development,production,test}byNODE_ENV, then.env.local(skipped whenNODE_ENV=test), then.env.{mode}.local. Values expand$VARreferences unless the$is escaped.- Bun invoked as
nodedoes not load.env(1.4.0) — underbun --bun,bunx --bun, or anodesymlink. Apackage.jsonscript callingnodeunderbun --bun runtherefore sees those variables asundefined. Pass--env-fileto keep them. - Disable automatic loading in production with
--no-env-fileorenv = false. Files named by--env-filestill load. bunfig.tomlparsing is strict (1.4.0): an unquoted string value, a missing newline between pairs, or an integer pastNumber.MAX_SAFE_INTEGERis aSyntaxErrorat startup.Bun.YAMLfollows YAML 1.2 (1.3.5), soyes,no,on, andoffparse as strings. Anon:key in a GitHub Actions workflow is the string"on".
Node Compatibility
From 1.4.0 Bun targets Node 26 and reports itself as such; 1.3.x reports Node 24. Most packages run unchanged. Reach for
a node: module deliberately in these cases:
node:sea— not implemented.bun build --compileis the replacement and a different API.node:test— partial. Writebun:test.node:cryptobeyond BoringSSL — noed448,x448,rsa-pss,dsa,dh,secp256k1, or the CCM, OCB, XTS, andchacha20-poly1305ciphers.node:async_hooks—AsyncLocalStorageworks but does not propagate intoWorker,MessagePort, orBroadcastChannel.createHookand the async-id APIs are stubs.- HTTP load balancing across processes —
node:clustershares an HTTP socket only on Linux, throughSO_REUSEPORT. node:v8serialize/deserialize— JavaScriptCore's wire format, not V8's, so a buffer does not travel between Bun and Node.- Loader hooks —
module.registeris a no-op. UseBun.plugin.
Read [${CLAUDE_SKILL_DIR}/references/node-compat.md] when porting a Node codebase, when a node: API behaves
differently under Bun, or when a package that works under Node fails — it carries the per-module gap list and the
fetch, Web API, and module-resolution behavior changes in 1.4.0 that break working code.
Application
When writing Bun, apply these conventions silently — do not narrate a rule while following it. Where existing code
uses a node: API that has a Bun-native equivalent, follow the codebase. Flag the alternative once.
When reviewing Bun, cite the violation and show the fix inline. Do not lecture.
Bad: "Bun's build API generally prefers rejection over a status flag, so you may want to..."
Good: if (!result.success) -> Bun.build() rejects; catch the AggregateError, or pass throw: false
Integration
The javascript and typescript skills own the language and the type system; this skill states only where Bun
departs from them. The nodejs skill owns what a node: module does — this skill owns only where Bun's
implementation is absent, partial, or behaves differently, and never teaches Node itself.
bun:test and bun install are this skill's, and the vitest skill owns Vitest. Which of them a project adopts is a
project decision, not a rule either skill states. The coding skill governs workflow.