in a static analysis step (--noEmit), while execution stays zero-config.
Step 2: Implement Native Testing Scaffolding
Bun's test runner mirrors Jest's assertion API but eliminates configuration overhead. AI assistants must be constrained to import from bun:test rather than installing external testing frameworks.
// test-suite.spec.ts
import { test, expect, describe, beforeAll, afterAll } from "bun:test";
import { createAppServer } from "../src/app";
describe("Application Lifecycle", () => {
let server: ReturnType<typeof createAppServer>;
beforeAll(() => {
server = createAppServer({ port: 3099 });
});
afterAll(() => {
server.stop();
});
test("handles health check endpoint", async () => {
const response = await fetch("http://localhost:3099/health");
expect(response.status).toBe(200);
const payload = await response.json();
expect(payload).toHaveProperty("status", "operational");
});
test("rejects malformed requests", async () => {
const response = await fetch("http://localhost:3099/api/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ invalid: true })
});
expect(response.status).toBe(400);
});
});
Architecture Decision: Use bun:test for all unit and integration tests. The runner executes in parallel by default, utilizes native async I/O, and requires zero configuration files. AI models should never generate jest.config.js, vitest.config.ts, or mocha setup scripts for Bun projects.
Bun's HTTP server replaces Express, Fastify, and http.createServer() for most use cases. It supports native WebSocket upgrades, request streaming, and zero-copy response handling.
// app-server.ts
import { serve, Server } from "bun";
export interface ServerOptions {
port: number;
hostname?: string;
websocket?: boolean;
}
export function createAppServer(options: ServerOptions): Server {
return serve({
port: options.port,
hostname: options.hostname ?? "0.0.0.0",
fetch(request: Request): Response {
const url = new URL(request.url);
if (url.pathname === "/health") {
return Response.json({ status: "operational", uptime: process.uptime() });
}
if (url.pathname === "/api/data" && request.method === "POST") {
return Response.json({ received: true }, { status: 201 });
}
return new Response("Not Found", { status: 404 });
},
websocket: options.websocket
? {
open(ws) {
console.log(`Client connected: ${ws.remoteAddress}`);
},
message(ws, data) {
ws.send(JSON.stringify({ echo: data }));
},
close(ws) {
console.log(`Client disconnected: ${ws.remoteAddress}`);
}
}
: undefined
});
}
Architecture Decision: Prefer Bun.serve() over third-party HTTP frameworks unless explicit Node.js middleware compatibility is required. The native server handles connection pooling, TLS termination, and WebSocket upgrades without additional dependencies. AI generation should skip express, fastify, or http module imports unless legacy migration dictates otherwise.
Step 4: Utilize Native I/O, Environment, and Shell APIs
File operations, environment variable access, and subprocess execution all have optimized native equivalents. AI models must be constrained to these APIs to avoid node:fs, dotenv, and child_process overhead.
// data-access.ts
import { Database } from "bun:sqlite";
export class DataStore {
private db: Database;
private configPath: string;
constructor(configPath: string) {
this.configPath = configPath;
this.db = new Database(":memory:");
this.initializeSchema();
}
private initializeSchema(): void {
this.db.run(`
CREATE TABLE IF NOT EXISTS records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
payload TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
}
async loadConfiguration(): Promise<Record<string, string>> {
const raw = await Bun.file(this.configPath).text();
return JSON.parse(raw);
}
async persistRecord(payload: string): Promise<number> {
const stmt = this.db.prepare("INSERT INTO records (payload) VALUES (?)");
const result = stmt.run(payload);
return result.lastInsertRowid as number;
}
async executeSystemCheck(): Promise<string> {
const proc = Bun.$`uname -r`;
return proc.text();
}
}
Architecture Decision:
Bun.file() and Bun.write() bypass Node.js stream overhead for synchronous and asynchronous file operations.
Bun.env auto-loads .env files at startup. No dotenv import or manual parsing required.
Bun.$ provides template-literal subprocess execution with built-in error handling and streaming. It replaces child_process.exec and spawn for most CLI automation tasks.
bun:sqlite ships with a compiled native driver. Installing better-sqlite3 or sqlite3 adds binary compilation steps and increases bundle size without performance gains.
Pitfall Guide
1. Runtime Compilation Fallacy
Explanation: AI assistants frequently generate tsc build scripts or ts-node execution commands, assuming TypeScript requires pre-compilation. Bun compiles TypeScript in-memory during execution.
Fix: Remove all tsc runtime scripts. Use bun run file.ts for execution. Reserve bunx tsc --noEmit strictly for static type validation in CI pipelines.
2. File System API Fragmentation
Explanation: Mixing node:fs with Bun.file() creates inconsistent error handling and performance characteristics. node:fs uses callback/promisified streams, while Bun.file() leverages zero-copy memory mapping.
Fix: Standardize on Bun.file() for reads and Bun.write() for writes. Only fall back to node:fs when interacting with legacy libraries that explicitly require Node.js stream interfaces.
3. Environment Variable Overhead
Explanation: Importing dotenv or manually parsing .env files adds unnecessary initialization steps. Bun automatically injects environment variables from .env files present in the working directory.
Fix: Remove dotenv dependencies. Access variables directly via Bun.env.VAR_NAME. Ensure .env files are committed to version control only in development environments; use CI/CD secret injection for production.
4. Subprocess Blocking Patterns
Explanation: Using child_process.execSync() or spawn() blocks the event loop or requires complex stream handling. AI models default to these patterns due to Node.js training data.
Fix: Replace with Bun.$ template literals for synchronous-style execution. Use Bun.spawn() for long-running processes requiring stream piping. The template API handles exit codes, stdout/stderr capture, and error throwing automatically.
5. Database Driver Redundancy
Explanation: Installing better-sqlite3 or sqlite3 introduces native compilation steps, platform-specific binaries, and increased Docker image size. Bun includes a pre-compiled SQLite driver.
Fix: Import directly from bun:sqlite. Use the Database class for synchronous operations and Database#query() for prepared statements. Only use external drivers if specific ORM integrations require Node.js compatibility layers.
6. Watch Mode Misconfiguration
Explanation: AI assistants generate nodemon, chokidar, or tsx --watch configurations for development hot-reloading. These tools poll the filesystem or rely on Node.js watchers, increasing CPU usage.
Fix: Use bun --watch run src/index.ts for file-change restarts. Use bun --hot run src/index.ts for HTTP servers requiring hot module replacement. Both leverage native OS event loops without external dependencies.
7. Dependency Priority Inversion
Explanation: AI models prioritize npm packages over native APIs because npm packages appear more frequently in training data. This leads to installing ws for WebSockets, express for routing, or jest for testing.
Fix: Enforce a strict decision hierarchy: (1) Check for Bun built-in, (2) Check for Bun-native API, (3) Fall back to npm only when native coverage is absent. Document this hierarchy in the AI context file to override statistical bias.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Greenfield API Service | Bun.serve() + bun:test + bun:sqlite | Zero-config runtime, native performance, minimal dependencies | Low (reduced CI time, smaller images) |
| Legacy Node.js Migration | Gradual node:fs/child_process retention + bun install | Maintains compatibility while adopting package manager and execution speed | Medium (temporary dual-API maintenance) |
| High-Throughput WebSocket Gateway | Bun.serve() with native websocket handler | Zero-copy message routing, built-in connection pooling | Low (eliminates ws/socket.io overhead) |
| CI/CD Pipeline Optimization | bun install + bun test + bunx tsc --noEmit | Parallel execution, binary lockfile, native TS validation | High (30-60% pipeline time reduction) |
| Cross-Runtime Compatibility Requirement | Retain express/jest + bun run | Ensures deployment flexibility across Node.js and Bun environments | Medium (increased bundle size, slower startup) |
Configuration Template
# Bun Runtime Context Configuration
## Runtime Declaration
This project uses Bun as the primary runtime, package manager, bundler, and test runner.
Do NOT generate Node.js-specific patterns or legacy toolchains.
## Execution Model
- TypeScript runs natively: `bun run src/file.ts`
- Type checking only: `bunx tsc --noEmit`
- No `tsc` build steps, `ts-node`, or `tsx` for runtime execution
## Package Management
- Install dependencies: `bun add <package>`
- Install dev dependencies: `bun add -d <package>`
- Install all: `bun install`
- Lock file: `bun.lockb` (binary format, do not edit manually)
- Do NOT use `npm`, `yarn`, or `pnpm` commands
## Native APIs (Priority Order)
1. Testing: `import { test, expect, describe } from "bun:test"`
2. HTTP/WebSocket: `Bun.serve({ fetch, websocket })`
3. File I/O: `Bun.file(path).text()`, `Bun.write(path, data)`
4. Environment: `Bun.env.VAR_NAME` (auto-loads `.env`)
5. Shell: `Bun.$`command`` or `Bun.spawn()`
6. SQLite: `import { Database } from "bun:sqlite"`
7. Bundling: `bun build ./src/index.ts --outdir ./dist`
8. Dev Modes: `bun --watch run` (restart), `bun --hot run` (HMR)
## Prohibited Packages
- jest, vitest, mocha
- webpack, vite, esbuild (unless explicitly required)
- nodemon, chokidar, tsx watch
- dotenv
- better-sqlite3, sqlite3
- express, fastify (unless Node.js middleware compatibility is mandatory)
## Generation Decision Order
1. Does a Bun built-in exist? Use it.
2. Does a Bun-native API replace an npm package? Use it.
3. Only then: use an npm package for features Bun does not cover.
Never skip steps 1-2 due to familiarity with Node.js equivalents.
Quick Start Guide
- Initialize Context File: Create
BUN_CONTEXT.md (or equivalent AI instruction file) in the project root. Paste the configuration template above. Ensure your AI assistant is configured to read this file before generating code.
- Clean Dependencies: Run
bun install to generate bun.lockb. Remove node_modules and legacy lockfiles (package-lock.json, yarn.lock). Delete any tsconfig.json build scripts that target runtime compilation.
- Validate Native Execution: Create a test file
src/verify.ts with a simple Bun.serve() or Bun.file() call. Execute with bun run src/verify.ts. Confirm zero compilation steps and native API resolution.
- Configure CI Pipeline: Replace
npm ci and tsc steps with bun install and bunx tsc --noEmit. Add bun test to the test stage. Verify parallel execution and binary lockfile integrity.
- Enforce Context Rules: Run a generation prompt for a new feature. Verify the output uses
bun:test, Bun.serve(), or Bun.$ instead of legacy equivalents. If drift occurs, refine the context file's prohibition list and decision hierarchy.