TypeScript + Bun
Category: Tech Stack · Areas: all
Description
Category
tech-stack
Areas
all
Slot
language-runtime
Components
- Language: TypeScript (strict mode)
- Runtime: Bun 1.x — NOT Node.js
- Package manager: Bun (
bun install,bun add) — NOT npm, NOT yarn, NOT pnpm - Linter + Formatter: Biome — NOT ESLint, NOT Prettier
- Test runner:
bun:test— NOT Vitest, NOT Jest - Workspace layout: Bun workspaces (
workspacesin rootpackage.json) - TypeScript config: strict,
noUncheckedIndexedAccess,exactOptionalPropertyTypes
Constraints
- All code must pass
tsc --noEmit(orbun run typecheck) with strict config - All code must pass Biome lint + format check (
bun run lint) - Scripts use
bun run, notnpm run - Use Bun-native APIs:
Bun.serve()for HTTP (not@hono/node-serveror similar Node adapters),Bun.file()for file I/O,Bun.spawn()for subprocesses - Do not use
tsx,ts-node, or other TypeScript transpilers — Bun runs.tsnatively - Do not add an
engines.nodefield — this project targets Bun, not Node.js - No
package-lock.jsonoryarn.lock— usebun.lock - No
node dist/index.jsstart commands — usebun src/index.ts - Biome config: indent style tabs, line width 100,
noUnusedImports: error
Drift Signals (anti-patterns to reject in review)
npm runin scripts → must bebun runprettieroreslintdependencies → replace with Biomevitestorjest→ replace withbun:testtsxorts-node→ remove; Bun executes TypeScript natively@hono/node-serveror any*-node-*HTTP adapter → useBun.serve()node dist/start command → usebun src/engines.nodeconstraint → remove
When to use
TypeScript projects using Bun as the runtime and package manager. Applies to monorepos and single-package projects alike. If a project historically drifted to Node.js tooling (npm, tsx, prettier, vitest), the concern documents the target state and the drift signals above identify what needs correction.
Artifact Impact
Selecting this concern requires these artifacts to change (a selected concern absent from them is drift):
- ADR: TypeScript + Bun (Biome, bun:test) as the language-runtime — not Node/npm/ESLint/Vitest
- TD: strict tsconfig, Bun-native APIs, workspace layout, Biome config
ADR References
Practices by activity
Agents working in any of these activities inherit the practices below through runtime work context, such as a DDx bead context digest.
Requirements (Frame activity)
- All user stories involving TypeScript must assume Bun as runtime and package manager
- If a library dependency requires a Node.js adapter, flag it as a concern at framing — it may require a Bun-compatible alternative
Design
- Use Bun workspaces for monorepos:
"workspaces": ["packages/*"]in rootpackage.json - Separate packages by concern:
shared(types/schemas),server(API),web(frontend) - Use workspace references (
workspace:*) for cross-package dependencies - HTTP servers:
Bun.serve()— not Express, Fastify with Node adapter, or@hono/node-server - For Hono: use
honodirectly withBun.serve()export, not the node-server adapter
Implementation
- Run TypeScript directly:
bun src/index.ts— no build step required for server/CLI - Scripts in
package.jsonmust usebun run/bun test/bun add, notnpm run - Use Bun-native APIs:
- File I/O:
Bun.file(),Bun.write() - Subprocesses:
Bun.spawn(),Bun.spawnSync() - HTTP:
Bun.serve() - Environment:
Bun.env
- File I/O:
- TypeScript config:
strict,noUncheckedIndexedAccess,exactOptionalPropertyTypes,verbatimModuleSyntax - No
any— TypeScript strict mode is enforced - Formatting: Biome with tabs, line width 100
- Linting: Biome recommended rules +
noUnusedImports: error,noUnusedVariables: warn - Imports: use
typekeyword for type-only imports (import type { Foo })
Testing
- Framework:
bun:test(built-in) - Run:
bun test - Use
mock()frombun:testfor module mocking - Fake data:
@faker-js/fakeror equivalent — not static fixtures - Prefer stubs to mocks; verify behavior, not call sequences
- Integration tests can use real databases via
docker compose up -dor testcontainers
Quality Gates (pre-commit / CI)
bun test— all tests passbun run typecheck—tsc --noEmitpasses for all packagesbun run lint— Biome lint + format check passes- No
package-lock.jsoncommitted (indicates npm was used) bun.lockcommitted and up to date
Dependency Management
- Add:
bun add <pkg>(notnpm install) - Dev deps:
bun add -d <pkg> - Workspace deps: reference with
"workspace:*"in package.json - Lock file:
bun.lock(text format, committed) - Audit:
bun auditfor known vulnerabilities
Composed-Concern Friction (known)
- Bun-native APIs require the Bun runtime at execution, not just at install.
Modules like
bun:sqlite,Bun.serve(), andBun.file()resolve only when the process is the Bun runtime. A tool that shells out to Node — notablynext build/next startfrom thereact-nextjsconcern — fails to resolveimport { Database } from "bun:sqlite"because the Next.js build/runtime is Node, not Bun. - Fix: force the Bun runtime with
bun --bun. Run Next.js (and any tool that would otherwise spawn Node) underbun --bun run <script>sobun:*built-ins resolve. Without--bun,bun run next buildstill hands execution to Node and the import fails. - Alternative: keep
bun:sqliteout of the Next.js build/runtime path — isolate it in a separate Bun-runtime service/process and reach it over an interface the Next.js layer can call. Choose--bunfor a single-process app; choose isolation when the frontend must build/run under plain Node. - When both
typescript-bunandreact-nextjsare active, declare which resolution the project uses as a project override inconcerns.mdso the choice is explicit rather than rediscovered at build time.