Bun is a JavaScript runtime with a built-in HTTP server, SQLite driver, test runner, bundler, and package manager. Bun.serve() handles HTTP requests and WebSocket upgrades in a single call, running on Bun’s native Zig/JavaScriptCore implementation — significantly faster than Node.js on I/O-bound workloads. Bun.file() wraps file handles as BunFile objects with lazy streaming. bun:sqlite is a zero-dependency SQLite driver bound directly to Bun’s native layer — no npm package required. Bun’s test runner uses Jest-compatible expect matchers and runs TypeScript tests without compilation. Bun.Worker runs scripts in background threads with postMessage. Claude Code generates Bun HTTP servers, SQLite data layers, WebSocket handlers, test suites, and the package.json scripts for production Bun applications.
CLAUDE.md for Bun Projects
## Bun Stack
- Version: bun >= 1.1
- HTTP: Bun.serve({ fetch(req) { ... } }) — handles HTTP + WebSocket upgrades
- SQLite: import { Database } from "bun:sqlite" — built-in, no npm package
- Files: Bun.file(path) → BunFile — lazy, streaming, Response-compatible
- Test: bun test — Jest-compatible, no config needed, TypeScript native
- Workers: new Worker("./worker.ts") — Bun.Worker for CPU-bound tasks
- Dev: bun --hot server.ts — HMR without full restarts
- Build: Bun.build({ entrypoints, outdir }) — bundler API
HTTP Server
// server.ts — Bun HTTP server with routing
import { Database } from "bun:sqlite"
import { join } from "path"
const db = new Database("orders.db")
db.run(`
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY,
customer_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
total_cents INTEGER NOT NULL,
items TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`)
const server = Bun.serve({
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
async fetch(req: Request): Promise<Response> {
const url = new URL(req.url)
const method = req.method
// Static file serving
if (url.pathname.startsWith("/static/")) {
const filePath = join("public", url.pathname.slice(8))
const file = Bun.file(filePath)
if (await file.exists()) {
return new Response(file) // Bun streams file lazily
}
return new Response("Not found", { status: 404 })
}
// API routes
if (url.pathname === "/api/orders" && method === "GET") {
return handleListOrders(req, url)
}
if (url.pathname === "/api/orders" && method === "POST") {
return handleCreateOrder(req)
}
const orderMatch = url.pathname.match(/^\/api\/orders\/([^/]+)$/)
if (orderMatch) {
const orderId = orderMatch[1]
if (method === "GET") return handleGetOrder(orderId)
if (method === "PATCH") return handleUpdateOrder(orderId, req)
if (method === "DELETE") return handleDeleteOrder(orderId)
}
return new Response("Not found", { status: 404 })
},
// WebSocket handler — upgraded from HTTP
websocket: {
message(ws, message) {
ws.send(`Echo: ${message}`)
},
open(ws) {
console.log("WebSocket connected")
},
close(ws) {
console.log("WebSocket disconnected")
},
},
error(error) {
console.error(error)
return new Response("Internal server error", { status: 500 })
},
})
console.log(`Server running at http://localhost:${server.port}`)
SQLite Data Layer
// lib/db.ts — bun:sqlite data access
import { Database, Statement } from "bun:sqlite"
import { randomUUID } from "crypto"
const db = new Database("orders.db", { create: true })
// Enable WAL mode for better concurrent read performance
db.run("PRAGMA journal_mode = WAL")
db.run("PRAGMA foreign_keys = ON")
// Prepared statements — cached, type-safe
const statements = {
insertOrder: db.prepare(`
INSERT INTO orders (id, customer_id, status, total_cents, items)
VALUES ($id, $customerId, 'pending', $totalCents, $items)
`),
getOrder: db.prepare(`
SELECT * FROM orders WHERE id = $id
`),
listOrders: db.prepare(`
SELECT * FROM orders
WHERE customer_id = $customerId
ORDER BY created_at DESC
LIMIT $limit OFFSET $offset
`),
updateStatus: db.prepare(`
UPDATE orders SET status = $status WHERE id = $id
`),
deleteOrder: db.prepare(`
DELETE FROM orders WHERE id = $id
`),
}
interface CreateOrderInput {
customerId: string
items: OrderItem[]
totalCents: number
}
interface OrderRow {
id: string
customer_id: string
status: string
total_cents: number
items: string
created_at: string
}
function parseOrder(row: OrderRow): Order {
return {
id: row.id,
customerId: row.customer_id,
status: row.status as Order["status"],
totalCents: row.total_cents,
items: JSON.parse(row.items),
createdAt: row.created_at,
}
}
export function createOrder(input: CreateOrderInput): Order {
const id = randomUUID()
// Transactions for atomicity
const insert = db.transaction(() => {
statements.insertOrder.run({
$id: id,
$customerId: input.customerId,
$totalCents: input.totalCents,
$items: JSON.stringify(input.items),
})
return statements.getOrder.get({ $id: id }) as OrderRow
})
return parseOrder(insert())
}
export function getOrder(orderId: string): Order | null {
const row = statements.getOrder.get({ $id: orderId }) as OrderRow | null
return row ? parseOrder(row) : null
}
export function listOrders(
customerId: string,
limit = 20,
offset = 0
): Order[] {
const rows = statements.listOrders.all({
$customerId: customerId,
$limit: limit,
$offset: offset,
}) as OrderRow[]
return rows.map(parseOrder)
}
Request Handlers
// lib/handlers.ts — HTTP request handlers
import { createOrder, getOrder, listOrders, updateOrderStatus } from "./db"
export async function handleListOrders(req: Request, url: URL): Promise<Response> {
const customerId = url.searchParams.get("customer_id")
if (!customerId) {
return Response.json({ error: "Missing customer_id" }, { status: 400 })
}
const limit = parseInt(url.searchParams.get("limit") ?? "20")
const offset = parseInt(url.searchParams.get("offset") ?? "0")
const orders = listOrders(customerId, limit, offset)
return Response.json({ orders, limit, offset })
}
export async function handleCreateOrder(req: Request): Promise<Response> {
let body: unknown
try {
body = await req.json()
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 })
}
// Simple validation
const { customerId, items } = body as any
if (!customerId || !Array.isArray(items) || items.length === 0) {
return Response.json({ error: "customerId and items required" }, { status: 422 })
}
const totalCents = items.reduce(
(sum: number, item: any) => sum + item.priceCents * item.quantity,
0
)
const order = createOrder({ customerId, items, totalCents })
return Response.json(order, { status: 201 })
}
export async function handleGetOrder(orderId: string): Promise<Response> {
const order = getOrder(orderId)
if (!order) return Response.json({ error: "Not found" }, { status: 404 })
return Response.json(order)
}
Test Suite
// tests/orders.test.ts — bun:test native test runner
import { describe, test, expect, beforeEach, afterAll } from "bun:test"
import { Database } from "bun:sqlite"
// Use in-memory SQLite for tests
const testDb = new Database(":memory:")
describe("Order creation", () => {
beforeEach(() => {
testDb.run("DELETE FROM orders")
})
test("creates order with correct total", () => {
const order = createOrderWithDb(testDb, {
customerId: "cust-123",
items: [
{ productId: "p1", quantity: 2, priceCents: 999 },
{ productId: "p2", quantity: 1, priceCents: 1499 },
],
totalCents: 3497,
})
expect(order.customerId).toBe("cust-123")
expect(order.totalCents).toBe(3497)
expect(order.status).toBe("pending")
expect(order.items).toHaveLength(2)
})
test("retrieves created order", () => {
const created = createOrderWithDb(testDb, {
customerId: "cust-456",
items: [{ productId: "p1", quantity: 1, priceCents: 500 }],
totalCents: 500,
})
const retrieved = getOrderWithDb(testDb, created.id)
expect(retrieved).not.toBeNull()
expect(retrieved!.id).toBe(created.id)
})
test("HTTP server returns 400 for missing customer_id", async () => {
const response = await fetch("http://localhost:3000/api/orders")
expect(response.status).toBe(400)
const body = await response.json()
expect(body.error).toInclude("customer_id")
})
})
afterAll(() => {
testDb.close()
})
Worker Thread for CPU Work
// workers/report-generator.ts — Bun.Worker for CPU-bound tasks
// This runs in a separate thread
self.onmessage = async (event: MessageEvent) => {
const { orderId, type } = event.data
if (type === "generate-report") {
// CPU-intensive work without blocking main thread
const data = await fetchOrderData(orderId)
const pdf = await generatePdf(data)
self.postMessage({ type: "report-ready", pdf: pdf.buffer }, [pdf.buffer])
}
}
// main thread usage
const worker = new Worker("./workers/report-generator.ts")
worker.onmessage = (event) => {
if (event.data.type === "report-ready") {
const pdfBuffer = event.data.pdf
// Send to client
}
}
worker.postMessage({ orderId: "ord-123", type: "generate-report" })
For the Node.js Fastify or Hono framework that provides similar routing ergonomics on Node.js with a larger ecosystem of middleware, the Express and Fastify guide covers production Node.js server patterns. For the Deno HTTP server with built-in KV and the same TypeScript-native runtime philosophy as Bun with different runtime APIs, see the Deno KV guide for the Deno runtime stack. The Claude Skills 360 bundle includes Bun skill sets covering HTTP server setup, SQLite integration, and the test runner. Start with the free tier to try Bun server generation.