Bun is a JavaScript runtime, bundler, test runner, and package manager in one binary — written in Zig for native performance. It runs TypeScript and JSX natively (no compile step), starts faster than Node.js, and ships built-in SQLite, WebSocket, file I/O, and a Fetch-based HTTP server. Claude Code writes Bun-native code: Bun.serve() HTTP handlers, bun:sqlite queries, bun test spec files, and the Bun.build() bundler configs that replace webpack and esbuild setups.
CLAUDE.md for Bun Projects
## Runtime Stack
- Runtime: Bun 1.x (latest stable)
- Language: TypeScript (natively executed — no tsc in build path)
- HTTP: Bun.serve() with Response API
- Database: bun:sqlite for local dev/testing; postgres for production via bun-sql
- Testing: bun test (built-in Jest-compatible runner)
- Bundler: Bun.build() for frontend assets
- Package manager: bun (not npm/pnpm — use bun.lockb)
- Scripts: bun run (not npm run)
HTTP Server with Bun.serve()
// src/server.ts — Bun's built-in HTTP server
import { type BunFile } from 'bun';
const PORT = parseInt(process.env.PORT ?? '3000');
const server = Bun.serve({
port: PORT,
async fetch(req: Request): Promise<Response> {
const url = new URL(req.url);
// Route matching
if (req.method === 'GET' && url.pathname === '/health') {
return Response.json({ status: 'ok', uptime: process.uptime() });
}
if (req.method === 'POST' && url.pathname === '/api/orders') {
return handleCreateOrder(req);
}
if (req.method === 'GET' && url.pathname.startsWith('/api/orders/')) {
const orderId = url.pathname.slice('/api/orders/'.length);
return handleGetOrder(orderId);
}
return new Response('Not Found', { status: 404 });
},
error(err: Error): Response {
console.error('Unhandled error:', err);
return Response.json({ error: 'Internal server error' }, { status: 500 });
},
});
console.log(`Listening on http://localhost:${server.port}`);
async function handleCreateOrder(req: Request): Promise<Response> {
const body = await req.json() as { userId: string; items: unknown[] };
if (!body.userId || !Array.isArray(body.items)) {
return Response.json({ error: 'userId and items are required' }, { status: 400 });
}
const order = await db.createOrder(body);
return Response.json(order, { status: 201 });
}
Built-in SQLite (bun:sqlite)
// src/db/sqlite.ts — zero-config SQLite, no driver install
import { Database } from 'bun:sqlite';
const db = new Database('orders.db', { create: true });
// WAL mode for concurrent reads
db.run('PRAGMA journal_mode = WAL');
db.run('PRAGMA foreign_keys = ON');
// Schema setup
db.run(`
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
user_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
total_cents INTEGER NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
)
`);
// Prepared statements for speed
const insertOrder = db.prepare<{ id: string }, [string, number]>(
'INSERT INTO orders (user_id, total_cents) VALUES (?, ?) RETURNING id'
);
const getOrder = db.prepare<{ id: string; status: string; total_cents: number }, [string]>(
'SELECT id, status, total_cents FROM orders WHERE id = ?'
);
const listOrdersByUser = db.prepare<{ id: string; status: string }, [string]>(
'SELECT id, status FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT 20'
);
export const ordersDb = {
create(userId: string, totalCents: number) {
const [row] = insertOrder.all(userId, totalCents);
return row;
},
get(id: string) {
return getOrder.get(id) ?? null;
},
listByUser(userId: string) {
return listOrdersByUser.all(userId);
},
// Transaction: multiple inserts atomically
createWithItems(userId: string, items: Array<{ productId: string; priceCents: number }>) {
return db.transaction(() => {
const totalCents = items.reduce((sum, i) => sum + i.priceCents, 0);
const [order] = insertOrder.all(userId, totalCents);
const insertItem = db.prepare(
'INSERT INTO order_items (order_id, product_id, price_cents) VALUES (?, ?, ?)'
);
for (const item of items) {
insertItem.run(order.id, item.productId, item.priceCents);
}
return order;
})();
},
};
Bun Test Runner
// src/__tests__/orders.test.ts — bun test (Jest-compatible)
import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { ordersDb } from '../db/sqlite';
// bun:test has built-in mocking
const mockEmailService = mock(() => Promise.resolve({ ok: true }));
describe('ordersDb', () => {
beforeEach(() => {
// Reset to fresh in-memory db for each test
// In practice, use :memory: db in test env
});
it('creates an order', () => {
const order = ordersDb.create('user-123', 4999);
expect(order.id).toBeString();
expect(order.id).toHaveLength(32);
});
it('creates order with items transactionally', () => {
const order = ordersDb.createWithItems('user-123', [
{ productId: 'prod-1', priceCents: 1999 },
{ productId: 'prod-2', priceCents: 3000 },
]);
const fetched = ordersDb.get(order.id);
expect(fetched?.total_cents).toBe(4999);
});
it('returns null for missing order', () => {
expect(ordersDb.get('nonexistent')).toBeNull();
});
});
// HTTP server integration test using Bun's built-in fetch
describe('API endpoints', () => {
it('POST /api/orders creates an order', async () => {
const res = await fetch('http://localhost:3000/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: 'user-123', items: [] }),
});
expect(res.status).toBe(201);
const body = await res.json();
expect(body.id).toBeDefined();
});
});
Bundler (Bun.build)
// build.ts — replace webpack/esbuild with Bun.build()
const result = await Bun.build({
entrypoints: ['./src/client/index.tsx'],
outdir: './dist',
target: 'browser',
minify: process.env.NODE_ENV === 'production',
sourcemap: 'external',
splitting: true, // Code splitting
naming: {
entry: '[name]-[hash].[ext]',
chunk: 'chunks/[name]-[hash].[ext]',
asset: 'assets/[name]-[hash].[ext]',
},
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'development'),
'process.env.API_URL': JSON.stringify(process.env.API_URL ?? ''),
},
plugins: [
// Bun plugins use the same esbuild plugin API
],
});
if (!result.success) {
for (const message of result.logs) {
console.error(message);
}
process.exit(1);
}
console.log(`Built ${result.outputs.length} files`);
Bun Shell API
// scripts/deploy.ts — Bun.$`shell commands` with full type safety
import { $ } from 'bun';
// Build and deploy script
async function deploy() {
console.log('Running tests...');
await $`bun test --bail`;
console.log('Building...');
await $`bun run build`;
// Capture output
const gitHash = (await $`git rev-parse --short HEAD`.text()).trim();
console.log(`Deploying ${gitHash}...`);
await $`wrangler pages deploy dist --branch main --commit-dirty=true`;
console.log(`Deployed ${gitHash}`);
}
deploy().catch(err => {
console.error(err);
process.exit(1);
});
package.json for Bun Projects
{
"name": "my-bun-app",
"scripts": {
"dev": "bun --watch src/server.ts",
"start": "bun src/server.ts",
"test": "bun test",
"test:watch": "bun test --watch",
"build": "bun run build.ts",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.0.0"
}
}
For the Deno runtime alternative with its permission model and built-in tools, the Deno/Bun guide covers both runtimes side by side. For deploying Bun applications to Cloudflare Workers (which runs a V8 isolate, not Bun directly), the Cloudflare Workers guide covers the Workers runtime. The Claude Skills 360 bundle includes Bun skill sets covering the HTTP server, SQLite, test runner, and bundler APIs. Start with the free tier to try Bun server generation.