Binding style rules for server-side TypeScript on Bun. This guide extends typescript/ for the Bun runtime. It is additive: where this guide is stricter, it wins for Bun code; the core guide remains the canonical language baseline.
The core guide is platform-agnostic. Bun has its own contract — a single-threaded event loop on JavaScriptCore, a process supervised by something that can restart it, an ecosystem of untyped dependencies, untrusted I/O at every edge, and a runtime that executes TypeScript by stripping types without ever checking them. Those impose rules the language guide cannot address.
This guide extends and defers to an ordered chain. Where they conflict, the higher authority wins:
- Bun official documentation — canonical. The API reference, the runtime and bundler semantics, the package manager and test runner contracts, the JavaScriptCore execution model. Where our guidance collides with documented runtime behaviour, the runtime wins.
- Community best practices — the production canon (the Node best-practices tradition, carried where Bun's
node:compatibility makes it apply). It fills the gaps the official docs leave open on production operation: error policy, security, project structure. Defer to it only where the official docs are silent. - This guide's overlay — the dexpace Bun layer: crash-only error policy, event-loop budgets, parse-every-boundary, dependency discipline, named lifecycles, typecheck as a separate gate.
The core typescript/ authority chain (Google → ts.dev → gts → its overlay) still governs the language layer. This guide adds the runtime layer on top; it does not replace it.
Bun is the dexpace default server runtime. Two facts shape how this guide treats it. First, there is no LTS policy: you pin the exact version (.bun-version plus the CI image) and upgrade deliberately, never floating. Second, Bun targets near-complete Node compatibility — most node: builtins work, so the runtime-agnostic doctrine carries directly, while the native surface (Bun.serve, Bun.SQL, bun:sqlite, Bun.build) is what this guide leans into. Node-only projects are the exception, not the rule; the retired Node guide lives at the tag node-guide-final for anyone who still needs it.
| # | Document | Scope |
|---|---|---|
| 01 | Runtime & Toolchain | bun install + committed bun.lock, exact version pin (.bun-version), bunfig.toml, native TS execution + tsc --noEmit gate, node: builtins, moduleResolution: "bundler", ESM-only, one process per container |
| 02 | Concurrency & the Event Loop | No blocking in request paths, no *Sync, event-loop lag measured, bounded Bun Workers for CPU, Web Streams with backpressure |
| 03 | HTTP Services | Hono on Bun.serve, thin handlers, zod validator middleware, centralized error mapping, correlation IDs, timeouts, app.request() injection testing, honest health checks |
| 04 | Persistence | Bun.SQL (Postgres) with Drizzle, bounded pools, parameterized tagged-template queries, bun:sqlite for embedded, transactions as units, rows parsed at the edge, migrations |
| 05 | Serialization & Validation | zod at every boundary, types from z.infer, never trust JSON.parse, Bun.env parsed once, null-vs-absent semantics, Bun-native file/S3 binary boundaries, no entity leakage |
| 06 | Logging | Structured JSON (pino on Bun), one logger, correlation via AsyncLocalStorage, PII redaction, levels, stdout-JSON fallback, no console.log |
| 07 | Bun Performance | bun:jsc profiling, Bun.nanoseconds(), mitata micro-benchmarks, allocation hygiene, Web Streams over buffer, Bun fetch pooling, JSC-vs-V8 carry/don't-carry, slowest-resource-first, measure before tuning |
| 08 | Build & Distribution | Bun.build for bundling, bun build --compile single-file executables, locked exports, publint + attw gate, api-extractor surface diff, bun publish to npm, changesets, reproducible builds |
Bun is a drop-in for much of the family's tooling, not all of it. Each row below records a core or family rule, the Bun substitution that replaces it for server code, and the intent that is preserved unchanged. The intent is the contract; the tool is the implementation.
| Core / family rule | Bun substitution | Preserved intent |
|---|---|---|
esbuild services (retired Node guide, tag node-guide-final) |
Bun.build |
One bundler for service builds, locked outputs, reproducible artifacts |
| V8 guidance (core 15.2–15.3) | JavaScriptCore profile (chapter 07) | Allocation hygiene, batching, and measure-first carry; V8 hidden-class specifics do not — see chapter 07's carry/don't-carry table |
bun install and bun test are family defaults since the core flip (core 1.5, 11.1) — they are recorded there, not substitutions here.
Counter-substitution. Server code uses bun test (core 11.1), but React component tests keep Vitest + Testing Library + MSW: bun test cannot run MSW and its DOM story is immature. That deviation belongs to the UI layer and is recorded in the typescript-react README ledger, not here.
These add to the 12 rules in the core guide. Each extends one core rule to the runtime's edges. When a BUN rule conflicts with a core one, the BUN rule wins for Bun code.
Step-by-step reasoning:
- An
unhandledRejectionoruncaughtExceptionmeans the process reached a state you did not design. You cannot reason about it, so you cannot safely continue from it. - Continuing is the dangerous option, not the safe one. A process limping on with a half-applied write or a corrupt in-memory cache produces silent wrong answers, which are worse than downtime.
- The handler does exactly three things: log the error with its full
causechain, flush the logger, exit with code 1. No recovery logic, no "try once more". - A supervisor (systemd, Kubernetes, pm2) owns liveness and restarts a clean process. Restartability is a design requirement, not an afterthought.
- This extends core 8: operational errors are still handled in place where they occur. Only programmer errors that escape all the way to the process boundary trigger the crash.
Step-by-step reasoning:
- One thread serves every request. A synchronous operation that takes 200ms does not slow one client by 200ms; it freezes every concurrent client for 200ms.
- So the loop is a bounded, shared resource and is treated like one. No
fs.*Sync,execSync, or synchronous crypto on any request path. - CPU-bound work (hashing, compression, large parses) moves to bounded Bun Workers or out of process entirely. The request thread stays free to dispatch.
- The budget is measured, not assumed. Event-loop lag is sampled, exported as a metric, and alerted before it surfaces as latency to clients.
- This extends core 15, performance from the outset, applied to the runtime's defining constraint.
Step-by-step reasoning:
- Anything crossing from outside into the domain is untyped until proven otherwise: environment variables, HTTP requests, queue messages, third-party responses, database rows.
- A raw
JSON.parseresult isanywearing a disguise. Trusting its shape is the same mistake asany, just relocated to runtime. - Each boundary value passes through a zod schema first. Invalid input is rejected at the edge, before any domain code touches it.
- The domain type is derived from the schema with
z.infer, never hand-written alongside it. One source of truth means the validator and the type cannot drift apart. - This extends core 10.7, parse at boundaries, from the API surface to every edge the runtime touches, including
Bun.envitself.
Step-by-step reasoning:
- Every dependency is code you did not write, running with your process's full privileges. The transitive tree is the real surface, not the top-level list.
- Keep the set minimal. Prefer the
node:standard library, Bun's native APIs, or a few lines of owned code over pulling a package and its subtree. - Pin versions with a committed
bun.lockand audit the tree withbun auditin CI — it scans the lockfile against the registry and exits non-zero on a vulnerability. An unaudited install is an unreviewed code change. - Every new dependency is justified in the PR that adds it: what it does, why a standard-library, Bun-native, or in-house alternative will not, who maintains it.
- This is the Bun face of the practices in security.md.
Step-by-step reasoning:
- Startup and shutdown are explicit and ordered, never implicit in import side effects. A named bootstrap acquires resources in sequence: config, then clients, then the server that depends on them.
- Shutdown is the reverse, triggered by
SIGTERM: stop accepting connections, drain in-flight work, close pools and consumers, then exit. - Shutdown is deadline-bounded. If draining exceeds a hard timeout, force exit rather than hang forever and let the orchestrator kill you.
- Every resource (pool, consumer, timer, server) binds its open and its close to these named phases. A resource with no owning phase is a leak waiting to happen.
- Verify it: send
SIGTERMto the running process and confirm clean drain logs. This extends core 13, resource management, to the process scope.
Step-by-step reasoning:
- Bun executes TypeScript by stripping types and running the result — "Bun does not perform typechecking." A file with a type error runs anyway, right up until the moment the lie reaches runtime.
- So type safety is not a side effect of running the code; it is a step you must add.
tsc --noEmitis a non-negotiable CI gate that fails the build on any type error, exactly because the runtime will not. - The family's
erasableSyntaxOnlystance is what makes type-stripping safe: noenum, no constructor parameter properties, nothing that needs runtime emit. Type-space and value-space stay cleanly separable, so stripping is lossless for behaviour. - This is a gate, not a runtime behaviour — it lives in CI and pre-commit, alongside
gts lint, never in the hot path. The ESM andnode:-prefix module mechanics that the Node guide once called out separately fold into chapter 01; Bun makes module-system mixing a non-issue. - This extends core 3, the type system as the first test suite — but on Bun the suite only runs if you wire it up.
Zero technical debt holds here as everywhere: what ships meets the design goals. Perfection over technical debt — debt never gets paid. A Bun service runs unattended for months; the shortcut taken today is the page at 3am later.
- Bun documentation — canonical runtime reference: the runtime, bundler, package manager, test runner, and JavaScriptCore execution model.
- Node.js best-practices tradition — community canon for production server operation, carried where Bun's
node:compatibility makes it apply. - zod — boundary parsing and
z.infer-derived types. - pino — structured, low-overhead logging.
- TigerBeetle Tiger Style — limits on everything, crash on unknown state, zero technical debt.