Bun Development
Complete Bun.js ecosystem guidance for runtime APIs, file I/O, package management, testing, SQLite, and security; use proactively when setting up Bun projects, replacing Node.js APIs with Bun-native APIs, writing bun test suites, implementing Bun.serve services, using bun:sqlite with prepared statements, configuring workspaces and lockfiles, hardening shell and SQL boundaries, or optimizing Bun performance and migration workflows.
npx skills add github:pantheon-org/tekhne --skill development--bun-developmentNavigation hub for Bun guidance focused on production-safe implementation.
Philosophy
- Prefer Bun-native APIs over Node.js shims: one runtime, one set of semantics.
- Treat every string that crosses a shell or SQL boundary as hostile until it is bound or passed as an argument.
- Keep installs deterministic: one package manager, one lockfile, no drift.
- Validate every change by running it, not by reading it.
When to Use
- You need Bun-native runtime APIs (
Bun.file,Bun.write,Bun.serve) - You are writing or debugging tests with
bun test - You are adding
bun:sqliteusage with transactions or prepared statements - You are setting dependency/workspace policy in a Bun project
- You need deterministic security guardrails for shell/SQL inputs
When Not to Use
- The project standard runtime is strictly Node.js with no Bun support
- You need framework-specific guidance not covered by Bun APIs
- The task is only package-agnostic JavaScript refactoring
Workflow
- Confirm Bun is available and lock dependency strategy.
- Choose the category (runtime, file I/O, testing, sqlite, package, security).
- Implement with copy/paste commands from Quick Commands.
- Apply anti-pattern checks before finalizing.
- If
bun installfails: checkbun.lock/bun.lockbfor merge conflicts, resolve them, and re-runbun install. - If tests fail after dependency changes: run
bun testto isolate regressions before proceeding.
- If
- Validate behavior with tests or execution checks.
- If validation fails: revert the last change, confirm the error, and address the specific failure before re-validating.
- For SQL validation (
rgfinds non-prepared statements in existing code): refactor each flagged call to usedb.prepare(...)with bound parameters before merging; do not leave raw interpolations in place. - Document decisions with links to exact references used.
Quick Commands
Verify Runtime
bun --version
Expected: a Bun version is printed.
Install Dependencies
bun install
Expected: lockfile is updated consistently (bun.lock/bun.lockb per project setup).
Run Script
bun run src/index.ts
Expected: script executes without Node.js runtime shims.
Execute Tests
bun test
Expected: failing assertions clearly identify behavior regressions.
Start HTTP Service
bun run server.ts
Expected: service binds configured port and handles requests via Bun.serve.
Validate SQL Pattern
rg -n "query\\(|prepare\\(" src
Expected: queries in new code paths use prepared statements where input is user-controlled. If raw query( calls with interpolated values are found in existing code, refactor them to db.prepare(...) with bound parameters before proceeding.
Categories by Priority
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Runtime & Core APIs | CRITICAL | runtime- |
| 2 | File I/O Operations | CRITICAL | file- |
| 3 | Testing Framework | HIGH | testing- |
| 4 | SQLite Integration | HIGH | sqlite- |
| 5 | Package Management | MEDIUM | package- |
| 6 | Security Practices | MEDIUM | security- |
Anti-Patterns
NEVER mix Node.js fs calls into Bun-native file workflows
WHY: mixed APIs create inconsistent behaviour and miss Bun performance advantages.
BAD:
import { readFileSync } from "node:fs";
const config = readFileSync("./config.json", "utf8");
GOOD:
const config = await Bun.file("./config.json").text();
NEVER run npm install in Bun-managed repositories
WHY: mixed package managers cause lockfile drift and nondeterministic installs.
BAD: npm install
GOOD: bun install
NEVER interpolate untrusted input into SQL strings
WHY: direct interpolation can introduce SQL injection vulnerabilities.
BAD:
db.query(`SELECT * FROM users WHERE email = '${email}'`).all();
GOOD:
db.prepare("SELECT * FROM users WHERE email = ?").all(email);
NEVER pass unescaped user input into shell commands
WHY: shell interpolation enables command injection.
BAD:
await Bun.spawn(["sh", "-c", userInput]).exited;
GOOD:
const safePath = Bun.file(userProvidedPath);
await safePath.text();
NEVER leave raw interpolated query( calls in existing code once flagged
WHY: the SQL validation step exists to find them, and an unrefactored hit is a live injection path.
BAD: merging while an interpolated db.query call that rg flagged is still present.
GOOD: refactor each flagged call to db.prepare(...) with bound parameters before merging.
NEVER ignore a lockfile merge conflict when bun install fails
WHY: an unresolved bun.lock/bun.lockb conflict leaves installs nondeterministic.
BAD: re-running bun install repeatedly without opening the lockfile.
GOOD: resolve the conflict markers, then re-run bun install.
NEVER skip bun test after changing dependencies
WHY: dependency changes can cause regressions that only a test run isolates.
BAD: updating packages and moving straight to the next step.
GOOD: run bun test straight after the change and fix regressions before proceeding.
NEVER keep going after a failed validation
WHY: stacking changes on a failing state hides which change broke behaviour.
BAD: continuing to edit while the validation error is unexplained.
GOOD: revert the last change, confirm the error, then address the specific failure before re-validating.
NEVER finalise a change without running the anti-pattern checks
WHY: the checks are the guard against the injection and package-manager mistakes above.
BAD: finishing straight after implementation.
GOOD: apply the anti-pattern checks (step 4 of the Workflow) before finalising.
NEVER record a decision without linking the reference you used
WHY: unlinked decisions cannot be re-checked when Bun behaviour changes.
BAD: “Used Bun.file because it is better.”
GOOD: “Used Bun.file per references/file-io-patterns.md.”
References
| Topic | Reference | When to Use |
|---|---|---|
| Runtime globals | references/runtime-globals.md |
Using Bun runtime globals |
| HTTP server | references/runtime-http-server.md |
Building services with Bun.serve |
| File I/O patterns | references/file-io-patterns.md |
Reading and writing files |
| Bun vs Node file APIs | references/file-vs-node.md |
Migrating from node:fs |
| Globbing | references/file-glob.md |
Matching files by pattern |
| Test runner | references/testing-bun-test.md |
Writing bun test suites |
| Matchers | references/testing-matchers.md |
Choosing assertions |
| Mocking | references/testing-mocking.md |
Mocking modules and functions |
| Snapshots | references/testing-snapshots.md |
Snapshot testing |
| SQLite | references/sqlite-basics.md |
Using bun:sqlite |
| Workspaces | references/pm-workspaces-agent-instructions.md |
Package and workspace policy |
| Shell safety | references/runtime-shell.md |
Running subprocesses safely |
| Passwords | references/runtime-password.md |
Hashing and verifying passwords |
External Links
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
Bun Glob Pattern Matching
Overview
Bun.Glob provides fast file pattern matching with support for globstar (**), wildcards (*), and character classes ([abc]).
Basic Usage
import { Glob } from "bun";
// Create glob pattern
const glob = new Glob("**/*.ts");
// Scan current directory
const files = glob.scan(".");
// Convert async iterator to array
const allFiles = await Array.fromAsync(files);
Glob Patterns
// Match all TypeScript files recursively
const tsGlob = new Glob("**/*.ts");
// Match specific directory
const srcGlob = new Glob("src/**/*.{ts,tsx}");
// Match at specific depth
const testGlob = new Glob("**/test/*.test.ts");
// Multiple extensions
const codeGlob = new Glob("**/*.{js,ts,jsx,tsx}");
Scanning with Options
const glob = new Glob("**/*.md");
// Scan with dot files
const withDot = glob.scan({
cwd: ".",
dot: true, // Include hidden files
absolute: false, // Relative paths
followSymlinks: false
});
// Absolute paths
const absolutePaths = glob.scan({
cwd: process.cwd(),
absolute: true
});
Filtering Results
const glob = new Glob("**/*.ts");
const files = glob.scan(".");
for await (const file of files) {
// Filter programmatically
if (!file.includes("node_modules")) {
console.log(file);
}
}
Performance Patterns
// Lazy iteration (memory efficient)
const glob = new Glob("**/*");
for await (const file of glob.scan(".")) {
await processFile(file);
}
// Collect all (use for small sets)
const allFiles = await Array.fromAsync(glob.scan("."));
// Count matches
let count = 0;
for await (const _ of glob.scan(".")) count++;
Common Use Cases
Find Test Files
const testGlob = new Glob("**/*.test.ts");
const testFiles = await Array.fromAsync(testGlob.scan("src"));
Find Config Files
const configGlob = new Glob("**/{.env,.env.*,*.config.{js,ts}}");
const configs = await Array.fromAsync(configGlob.scan("."));
Exclude Patterns
const glob = new Glob("**/*.ts");
const files = glob.scan(".");
for await (const file of files) {
// Manual exclusion
if (!file.includes("node_modules") && !file.includes("dist")) {
console.log(file);
}
}
Best Practices
- Use Bun.Glob instead of fs.readdir for pattern matching
- Prefer lazy iteration with for await for large file sets
- Use absolute paths when passing to Bun.file()
- Exclude node_modules and dist manually if needed
- Use specific patterns to reduce scan scope
Anti-Patterns
- Don’t use node:fs/promises for globbing
- Avoid Array.fromAsync for very large file sets (memory)
- Don’t scan from root without specific patterns
- Avoid overly broad patterns like */
Bun.file I/O Patterns
Reading Files
Basic File Reading
// Read as text
const file = Bun.file("./data.json");
const text = await file.text();
// Read as JSON
const json = await file.json();
// Read as ArrayBuffer
const buffer = await file.arrayBuffer();
// Read as Uint8Array
const bytes = await file.bytes();
Check File Exists
const file = Bun.file("./config.json");
if (await file.exists()) {
const config = await file.json();
} else {
console.log("File not found");
}
File Metadata
const file = Bun.file("./document.pdf");
console.log(file.size); // File size in bytes
console.log(file.type); // MIME type
console.log(file.name); // File name
Writing Files
Basic File Writing
// Write string
await Bun.write("output.txt", "Hello, Bun!");
// Write JSON
await Bun.write("data.json", { key: "value" });
// Write Blob
const blob = new Blob(["content"], { type: "text/plain" });
await Bun.write("file.txt", blob);
// Write Response
const response = await fetch("https://example.com/data");
await Bun.write("fetched.html", response);
// Copy file
await Bun.write("destination.txt", Bun.file("source.txt"));
Incremental Writing
const file = Bun.file("large-file.txt");
const writer = file.writer();
writer.write("First chunk\n");
writer.write("Second chunk\n");
writer.write("Third chunk\n");
await writer.end();
Streaming Large Files
const file = Bun.file("large-dataset.json");
const stream = file.stream();
for await (const chunk of stream) {
// Process chunk
console.log(chunk);
}
File Operations
Delete File
const file = Bun.file("temporary.txt");
await file.delete();
Use node:fs for Directories
import { mkdir, readdir } from "node:fs/promises";
// Create directory
await mkdir("output", { recursive: true });
// Read directory
const files = await readdir("./src");
Repo Patterns (from bun-file-io skill)
- Prefer Bun APIs over Node
fsfor file access - Check
Bun.file(...).exists()before reading - For binary/large files use
arrayBuffer()and MIME checks viafile.type - Use
Bun.Glob+Array.fromAsyncfor scans - For large writes, use
Bun.write(Bun.file(path), text) - Use
path.join/path.resolvefor paths - Prefer promise
.catch(...)overtry/catchwhen possible
Bun File APIs vs Node.js fs
When to Use Bun APIs
Prefer Bun for
- Reading file contents (text, JSON, binary)
- Writing files (strings, buffers, Blobs)
- Checking file existence
- File metadata (size, type, name)
- Pattern matching (Bun.Glob)
- Streaming file contents
Prefer Node.js fs for
- Directory operations (mkdir, readdir, rmdir)
- Recursive file operations
- File permissions (chmod)
- Symbolic links
- Directory watching
- Complex file system operations
Reading Files
// ✅ Bun (preferred)
const file = Bun.file("config.json");
const data = await file.json();
const text = await file.text();
const buffer = await file.arrayBuffer();
// ❌ Node.js (avoid unless necessary)
import { readFile } from "node:fs/promises";
const buffer = await readFile("config.json");
const text = buffer.toString();
Writing Files
// ✅ Bun (preferred)
await Bun.write("output.txt", "Hello, World!");
await Bun.write("data.json", JSON.stringify(obj));
await Bun.write("file.bin", arrayBuffer);
// ❌ Node.js (avoid unless necessary)
import { writeFile } from "node:fs/promises";
await writeFile("output.txt", "Hello, World!");
File Existence
// ✅ Bun (preferred)
const file = Bun.file("config.json");
const exists = await file.exists();
// ❌ Node.js (avoid)
import { access } from "node:fs/promises";
try {
await access("config.json");
// exists
} catch {
// doesn't exist
}
Directory Operations
// ✅ Node.js (required for directories)
import { mkdir, readdir, rm } from "node:fs/promises";
await mkdir("output", { recursive: true });
const entries = await readdir("src");
await rm("temp", { recursive: true, force: true });
// ❌ Bun (no directory APIs)
// Not available
File Metadata
// ✅ Bun (preferred for files)
const file = Bun.file("document.pdf");
console.log(file.size); // bytes
console.log(file.type); // MIME type
console.log(file.name); // filename
// ✅ Node.js (required for detailed stats)
import { stat } from "node:fs/promises";
const stats = await stat("document.pdf");
console.log(stats.mtime); // modified time
console.log(stats.mode); // permissions
Pattern Matching
// ✅ Bun (preferred)
import { Glob } from "bun";
const glob = new Glob("**/*.ts");
const files = await Array.fromAsync(glob.scan("."));
// ❌ Node.js (manual recursion needed)
import { readdir } from "node:fs/promises";
// Requires manual recursive implementation
Streaming
// ✅ Bun (preferred for file streams)
const file = Bun.file("large.log");
for await (const chunk of file.stream()) {
// Process chunk
}
// ❌ Node.js (more verbose)
import { createReadStream } from "node:fs";
const stream = createReadStream("large.log");
for await (const chunk of stream) {
// Process chunk
}
Decision Matrix
| Operation | Use Bun API | Use Node.js fs |
|---|---|---|
| Read file | ✅ | ❌ |
| Write file | ✅ | ❌ |
| Check exists | ✅ | ❌ |
| File metadata | ✅ | For detailed stats |
| Pattern match | ✅ | ❌ |
| Stream file | ✅ | ❌ |
| Create directory | ❌ | ✅ |
| List directory | ❌ | ✅ |
| Remove directory | ❌ | ✅ |
| File permissions | ❌ | ✅ |
| Symlinks | ❌ | ✅ |
| Watch files | ❌ | ✅ |
Best Practices
- Default to Bun APIs for file operations
- Use Node.js fs only when Bun doesn’t provide the functionality
- Don’t mix APIs unnecessarily - be consistent
- Leverage Bun’s lazy file loading (no upfront read)
- Use Bun.Glob instead of manual directory traversal
Anti-Patterns
- Using Node.js fs for simple read/write operations
- Mixing Bun.file() and fs APIs in same module
- Using fs.readdir for pattern matching (use Bun.Glob)
- Ignoring Bun’s lazy loading benefits
Bun Workspaces: Agent Instructions
Purpose
Use this guide when an agent needs to design, migrate, or maintain a Bun workspace monorepo. These instructions are generic and reusable across projects.
When To Use
Apply this workflow if the user asks to:
- split a codebase into packages/apps
- migrate from single-package to monorepo
- improve dependency ownership
- speed up targeted test/build/lint execution
- standardize versions across multiple packages
Core Principles
- Keep root package orchestration-only (
private: true). - Put runtime dependencies in the workspace that actually uses them.
- Use
"workspace:*"for internal package dependencies. - Prefer package imports over deep relative cross-package imports.
- Migrate in phases and verify after each phase.
Minimum Workspace Setup
Root package.json:
{
"private": true,
"workspaces": ["apps/*", "packages/*"]
}
Workspace package example:
{
"name": "@org/example-package",
"version": "0.1.0",
"type": "module",
"dependencies": {
"@org/shared": "workspace:*"
}
}
Agent Workflow
-
Inventory
- Identify domains/boundaries (app entrypoints, shared/core libs, feature modules).
- Map current dependency usage (who imports what, who owns runtime deps).
-
Propose target topology
apps/*for executables/services.packages/*for reusable libraries.- Define package names and ownership boundaries.
-
Create scaffolding
- Add root
workspaces. - Add workspace
package.jsonfiles. - Set internal deps via
"workspace:*".
- Add root
-
Migrate incrementally
- Move one boundary at a time.
- Update imports to package names.
- Keep API surfaces explicit (avoid wildcard barrels across package boundaries).
-
Verify each phase
- Run tests, typecheck, lint.
- Fix regressions before next move.
-
Finalize scripts/CI
- Replace repo scripts with workspace-aware commands.
- Add filtered workflows for targeted iteration.
Command Patterns (Bun)
# Install all workspaces
bun install
# Run in all workspaces that define the script
bun run --workspaces --if-present build
bun run --workspaces --if-present lint
bun run --parallel --workspaces --if-present test
# Run in selected workspace(s)
bun run --filter '@org/package-a' test
bun run --filter '@org/*' build
# Install for selected workspace(s)
bun install --filter '@org/*'
Catalog Guidance
Use catalog/catalogs in root package.json when many workspaces share the same dependency versions.
Root:
{
"catalog": {
"typescript": "^5.9.0",
"@types/node": "^25.0.0"
}
}
Workspace:
{
"devDependencies": {
"typescript": "catalog:typescript",
"@types/node": "catalog:@types/node"
}
}
Migration Safety Checklist
- Root is
private: true - All internal package dependencies use
"workspace:*" - No cross-workspace deep relative imports remain
- Runtime deps moved out of root to owning workspaces
- Workspace scripts run successfully with
--workspaces - Filtered runs work with
--filter - Tests/typecheck/lint pass after migration
Common Failures To Avoid
- Keeping dependencies centralized in root after split
- Moving all modules at once (hard to debug)
- Not defining clear package ownership boundaries
- Exporting overly broad wildcard APIs across package boundaries
- Skipping per-phase verification
Bun Global APIs
Bun provides globally available APIs optimized for performance that are significantly faster than Node.js equivalents. These APIs are available without imports.
Core Globals
Bun.file()
Fast file reading with automatic content-type detection and streaming support.
// Basic file reading
const file = Bun.file("./data.json");
const text = await file.text(); // Read as string
const json = await file.json(); // Parse JSON automatically
const buffer = await file.arrayBuffer(); // Read as ArrayBuffer
const blob = await file.blob(); // Read as Blob
// File metadata
console.log(file.name); // Filename
console.log(file.size); // Size in bytes
console.log(file.type); // MIME type (auto-detected)
// Check existence
if (await file.exists()) {
const content = await file.text();
}
// Streaming large files
const largeFile = Bun.file("./large-video.mp4");
const stream = largeFile.stream();
for await (const chunk of stream) {
// Process chunk by chunk
console.log(chunk);
}
Performance: 10x faster than fs.readFile for JSON parsing.
Bun.write()
High-performance file writing with automatic serialization.
// Write string
await Bun.write("output.txt", "Hello, Bun!");
// Write JSON (auto-stringify)
await Bun.write("data.json", { key: "value", count: 42 });
// Write Blob
const blob = new Blob(["content"], { type: "text/plain" });
await Bun.write("output.txt", blob);
// Write Response
const response = await fetch("https://example.com/data.json");
await Bun.write("downloaded.json", response);
// Write to specific path
await Bun.write(Bun.file("./output/result.txt"), "data");
// Append to file (not supported directly, use streams)
Performance: 3x faster than fs.writeFile.
Bun.serve()
Ultra-fast HTTP server with built-in WebSocket support.
const server = Bun.serve({
port: 3000,
hostname: "localhost",
fetch(req, server) {
const url = new URL(req.url);
// Handle routes
if (url.pathname === "/") {
return new Response("Hello, World!");
}
if (url.pathname === "/api/data") {
return Response.json({
message: "Fast JSON response",
timestamp: Date.now()
});
}
// Serve static files
if (url.pathname.startsWith("/static/")) {
const filePath = `.${url.pathname}`;
return new Response(Bun.file(filePath));
}
// WebSocket upgrade
if (url.pathname === "/ws") {
if (server.upgrade(req)) {
return; // Upgraded to WebSocket
}
return new Response("WebSocket upgrade failed", { status: 400 });
}
return new Response("Not Found", { status: 404 });
},
// WebSocket handlers
websocket: {
message(ws, message) {
console.log("Received:", message);
ws.send(`Echo: ${message}`);
},
open(ws) {
console.log("Client connected");
ws.subscribe("chat-room");
},
close(ws, code, reason) {
console.log("Client disconnected", code, reason);
},
},
// Server lifecycle
error(error) {
return new Response("Internal Server Error", { status: 500 });
},
});
console.log(`Server running on http://localhost:${server.port}`);
// Graceful shutdown
process.on("SIGINT", () => {
server.stop();
process.exit(0);
});
Performance: 4x faster than Node.js HTTP server, handles 100k+ req/s.
Bun.env
Type-safe environment variable access.
// Access environment variables
const apiKey = Bun.env.API_KEY;
const port = Bun.env.PORT ?? "3000";
const nodeEnv = Bun.env.NODE_ENV ?? "development";
// All environment variables
console.log(Bun.env);
// Type-safe with TypeScript
declare module "bun" {
interface Env {
API_KEY: string;
DATABASE_URL: string;
PORT?: string;
}
}
// Now TypeScript knows these exist
const dbUrl: string = Bun.env.DATABASE_URL; // Type-safe!
Prefer Bun.env over process.env for consistency.
Bun.$
Safe shell command execution with template literals.
import { $ } from "bun";
// Execute shell commands
const output = await $`ls -la`.text();
console.log(output);
// Get current git branch
const branch = await $`git branch --show-current`.text();
console.log(`Current branch: ${branch.trim()}`);
// With error handling
try {
await $`npm run build`;
console.log("Build successful");
} catch (error) {
console.error("Build failed:", error);
}
// Pipe commands
const result = await $`cat package.json | grep "version"`.text();
// Capture exit code
const proc = await $`exit 42`;
console.log(proc.exitCode); // 42
// Escape special characters automatically
const filename = "file with spaces.txt";
await $`cat ${filename}`; // Safely escaped
Security: Automatically escapes variables to prevent injection.
Bun.password
Built-in password hashing with bcrypt/argon2.
// Hash password
const password = "super-secret-password";
const hash = await Bun.password.hash(password);
console.log(hash); // $argon2id$...
// Verify password
const isValid = await Bun.password.verify(password, hash);
console.log(isValid); // true
// Custom algorithm (default is argon2id)
const bcryptHash = await Bun.password.hash(password, {
algorithm: "bcrypt",
cost: 12,
});
// Verify works with any algorithm
const isBcryptValid = await Bun.password.verify(password, bcryptHash);
Security: No external dependencies needed (no bcrypt npm package).
File System Operations
Glob Pattern Matching
import { Glob } from "bun";
// Find all TypeScript files
const glob = new Glob("**/*.ts");
// Scan directory
for await (const file of glob.scan(".")) {
console.log(file);
}
// Get all matches as array
const tsFiles = await Array.fromAsync(glob.scan("."));
console.log(`Found ${tsFiles.length} TypeScript files`);
// Multiple patterns
const sourceFiles = new Glob("src/**/*.{ts,tsx}");
File Watching
import { watch } from "fs";
// Watch file or directory
const watcher = watch("./src", { recursive: true }, (event, filename) => {
console.log(`${event}: ${filename}`);
});
// Stop watching
watcher.close();
HTTP Client
Use native fetch (Web API standard):
// GET request
const response = await fetch("https://api.example.com/data");
const data = await response.json();
// POST request with JSON
const postResponse = await fetch("https://api.example.com/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Alice",
email: "alice@example.com",
}),
});
// Handle errors
if (!postResponse.ok) {
throw new Error(`HTTP error! status: ${postResponse.status}`);
}
// Download file
const fileResponse = await fetch("https://example.com/file.pdf");
await Bun.write("downloaded.pdf", fileResponse);
Performance Comparisons
| Operation | Node.js | Bun | Speedup |
|---|---|---|---|
| Read JSON file | 100ms | 10ms | 10x |
| Write file | 50ms | 17ms | 3x |
| HTTP req/s | 25k | 100k+ | 4x |
| Password hash | 300ms | 50ms | 6x |
| SQLite query | 50ms | 5ms | 10x |
Best Practices
Use Bun.file() for All File Operations
// GOOD - Fast Bun API
const data = await Bun.file("data.json").json();
await Bun.write("output.json", { result: data });
// AVOID - Slower Node.js APIs
import fs from "fs/promises";
const text = await fs.readFile("data.json", "utf-8");
const data = JSON.parse(text);
await fs.writeFile("output.json", JSON.stringify({ result: data }));
Handle File Existence
// GOOD - Check before reading
const file = Bun.file("config.json");
if (await file.exists()) {
const config = await file.json();
} else {
console.error("Config file not found");
}
// AVOID - Catching errors
try {
const config = await Bun.file("config.json").json();
} catch {
// File doesn't exist or invalid JSON?
}
Structured Error Handling
// GOOD - Specific error handling
try {
const file = Bun.file("data.json");
if (!(await file.exists())) {
throw new Error("File not found");
}
const data = await file.json();
if (!data.version) {
throw new Error("Invalid data format");
}
return data;
} catch (error) {
if (error instanceof SyntaxError) {
console.error("Invalid JSON");
} else if (error instanceof Error) {
console.error(error.message);
}
}
Stream Large Files
// GOOD - Stream large files
const largeFile = Bun.file("video.mp4");
const stream = largeFile.stream();
const response = new Response(stream, {
headers: {
"Content-Type": "video/mp4",
"Content-Length": largeFile.size.toString(),
},
});
// AVOID - Loading entire file in memory
const largeFile = Bun.file("video.mp4");
const buffer = await largeFile.arrayBuffer(); // OOM risk!
Common Pitfalls
Async/Await Required
// WRONG - Missing await
const text = Bun.file("data.txt").text(); // Returns Promise!
console.log(text); // Promise { <pending> }
// CORRECT
const text = await Bun.file("data.txt").text();
console.log(text); // Actual content
File Path Resolution
// WRONG - Relative to current directory (may change)
const file = Bun.file("data.json");
// CORRECT - Relative to script location
import { fileURLToPath } from "url";
import { dirname, join } from "path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const file = Bun.file(join(__dirname, "data.json"));
Response Body Can Only Be Read Once
// WRONG - Body already consumed
const response = await fetch("https://example.com/api");
const json1 = await response.json();
const json2 = await response.json(); // ERROR!
// CORRECT - Clone response if needed twice
const response = await fetch("https://example.com/api");
const clone = response.clone();
const json1 = await response.json();
const json2 = await clone.json();
Related References
- @see file-io-patterns.md - File I/O best practices
- @see runtime-http-server.md - Bun.serve() details
- @see runtime-shell.md - Bun.$ command execution
- @see security-shell-escaping.md - Preventing injection attacks
HTTP Server
Bun’s HTTP server (Bun.serve) is optimized for speed with built-in WebSocket support.
Basic HTTP Server
const server = Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response("Hello Bun!");
}
if (url.pathname === "/api/users") {
return Response.json({ users: ["Alice", "Bob"] });
}
return new Response("Not Found", { status: 404 });
},
});
console.log(`Server running on http://localhost:${server.port}`);
Request Handling
JSON Requests
fetch(req) {
if (req.method === "POST") {
const body = await req.json();
// Process body
return Response.json({ success: true });
}
}
Form Data & File Uploads
fetch(req) {
const formData = await req.formData();
const file = formData.get("file") as File;
await Bun.write(`uploads/${file.name}`, file);
return Response.json({ uploaded: file.name });
}
Headers
fetch(req) {
const auth = req.headers.get("Authorization");
return new Response("OK", {
headers: {
"Content-Type": "application/json",
"X-Custom-Header": "value",
},
});
}
WebSocket Support
const server = Bun.serve({
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === "/ws") {
// Upgrade to WebSocket
const success = server.upgrade(req);
if (success) return undefined;
return new Response("WebSocket upgrade failed", { status: 500 });
}
return new Response("Use /ws for WebSocket");
},
websocket: {
open(ws) {
console.log("Client connected");
ws.send("Welcome!");
},
message(ws, message) {
console.log("Received:", message);
ws.send(`Echo: ${message}`);
},
close(ws) {
console.log("Client disconnected");
},
},
});
Server Options
Bun.serve({
port: 3000,
hostname: "127.0.0.1", // Bind to localhost only
development: Bun.env.NODE_ENV !== "production",
// Request body size limit
maxRequestBodySize: 1024 * 1024 * 10, // 10MB
fetch(req) {
return new Response("OK");
},
error(error) {
return new Response(`Server Error: ${error.message}`, {
status: 500
});
},
});
Best Practices
- Validate request paths to prevent traversal attacks
- Set maxRequestBodySize to prevent DoS
- Bind to 127.0.0.1 for dev, configure for production
- Authenticate before WebSocket upgrade
- Use Response.json() for JSON responses
- Handle errors with custom error handler
Common Patterns
RESTful API
fetch(req) {
const url = new URL(req.url);
const route = url.pathname;
const method = req.method;
if (route === "/api/items" && method === "GET") {
return Response.json({ items: getItems() });
}
if (route.startsWith("/api/items/") && method === "GET") {
const id = route.split("/")[3];
return Response.json(getItem(id));
}
if (route === "/api/items" && method === "POST") {
const item = await req.json();
return Response.json(createItem(item), { status: 201 });
}
return new Response("Not Found", { status: 404 });
}
Static File Serving
fetch(req) {
const url = new URL(req.url);
const filepath = `./public${url.pathname}`;
const file = Bun.file(filepath);
if (await file.exists()) {
return new Response(file);
}
return new Response("Not Found", { status: 404 });
}
Anti-patterns
- DON’T bind to 0.0.0.0 in development unnecessarily
- DON’T forget to handle 404s
- DON’T upgrade WebSocket without authentication
- DON’T ignore maxRequestBodySize (DoS risk)
References
Password Hashing
Bun provides secure password hashing with Bun.password using Argon2.
Hashing Passwords
// Hash a password (uses Argon2id by default)
const hash = await Bun.password.hash("user-password-123");
// Store hash in database
await db.run(
"INSERT INTO users (email, password_hash) VALUES (?, ?)",
[email, hash]
);
Verifying Passwords
// Retrieve stored hash from database
const user = await db.query("SELECT password_hash FROM users WHERE email = ?")
.get(email);
// Verify password
const isValid = await Bun.password.verify("user-password-123", user.password_hash);
if (isValid) {
// Password matches - login success
} else {
// Password doesn't match - login failed
}
Algorithm Details
Bun.password uses Argon2id by default:
const hash = await Bun.password.hash("password", {
algorithm: "argon2id", // default
memoryCost: 19456, // memory in KB
timeCost: 2, // iterations
});
Supported algorithms:
argon2id(default, recommended)argon2iargon2dbcrypt
Best Practices
- Always hash passwords - never store plaintext
- Use the defaults - Bun’s defaults are secure
- Verify with timing-safe comparison - Bun.password.verify is timing-safe
- Don’t log hashes - treat like passwords
- Rate-limit login attempts - prevent brute force
Complete Authentication Example
import { Database } from "bun:sqlite";
const db = new Database("users.db");
// Create users table
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL
)
`);
// Register user
async function registerUser(email: string, password: string) {
const hash = await Bun.password.hash(password);
try {
db.run(
"INSERT INTO users (email, password_hash) VALUES (?, ?)",
[email, hash]
);
return { success: true };
} catch (error) {
return { success: false, error: "Email already exists" };
}
}
// Login user
async function loginUser(email: string, password: string) {
const user = db.query("SELECT * FROM users WHERE email = ?").get(email);
if (!user) {
return { success: false, error: "Invalid credentials" };
}
const isValid = await Bun.password.verify(password, user.password_hash);
if (isValid) {
return { success: true, userId: user.id };
} else {
return { success: false, error: "Invalid credentials" };
}
}
// Usage
await registerUser("user@example.com", "securePassword123");
const result = await loginUser("user@example.com", "securePassword123");
console.log(result); // { success: true, userId: 1 }
Security Audit
Check for plaintext password storage:
# Find potential plaintext password storage
rg "password\s*=.*req\.(body|json)" --type ts
# Find missing password hashing
rg "INSERT.*password.*VALUES" --type ts
# Ensure Bun.password.hash is used
rg "Bun\.password\.hash" --type ts
Anti-patterns
- DON’T store passwords in plaintext
- DON’T use weak hashing (MD5, SHA1)
- DON’T implement your own crypto
- DON’T expose password hashes in logs or API responses
- DON’T skip rate limiting on login endpoints
Migration from bcrypt
// Old bcrypt code
import bcrypt from "bcrypt";
const hash = await bcrypt.hash(password, 10);
const isValid = await bcrypt.compare(password, hash);
// New Bun.password code
const hash = await Bun.password.hash(password);
const isValid = await Bun.password.verify(password, hash);
References
Shell Command Execution
Bun provides safe shell command execution with the Bun.$ tagged template.
Bun.$ Basics
The $ tagged template automatically escapes arguments to prevent command injection:
const userInput = "file.txt; rm -rf /";
// SAFE: userInput is escaped
const output = await Bun.$`cat ${userInput}`.text();
// Equivalent to: cat 'file.txt; rm -rf /'
Command Execution
Capture Output
// Text output
const stdout = await Bun.$`ls -la`.text();
// Raw bytes
const bytes = await Bun.$`cat binary.dat`.bytes();
// JSON output
const data = await Bun.$`npm list --json`.json();
// Stream output
const stream = Bun.$`tail -f app.log`.readable();
Exit Code & Stderr
const result = await Bun.$`npm test`;
console.log(result.exitCode); // 0 for success
console.log(result.stdout); // standard output
console.log(result.stderr); // error output
Command Chaining
// Sequential execution
await Bun.$`git add . && git commit -m ${message} && git push`;
// Pipe commands
const output = await Bun.$`cat file.txt | grep "pattern"`.text();
Bun.spawn Alternative
For more control, use Bun.spawn:
const proc = Bun.spawn(["git", "status"], {
stdout: "pipe",
stderr: "pipe",
});
const stdout = await Bun.readableStreamToText(proc.stdout);
const stderr = await Bun.readableStreamToText(proc.stderr);
await proc.exited;
console.log("Exit code:", proc.exitCode);
Bun.which
Find binary paths:
const gitPath = await Bun.which("git");
// => "/usr/bin/git"
if (gitPath) {
await Bun.$`${gitPath} status`;
}
Security Considerations
SAFE: Bun.$ Tagged Template
// Arguments are ALWAYS escaped
await Bun.$`rm ${filename}`;
CRITICAL: bash -c Bypass
// DON'T: This bypasses escaping!
await Bun.$`bash -c "rm ${filename}"`; // VULNERABLE
// Attacker can inject: file.txt; rm -rf /
HIGH: Argument Injection
const userInput = "--help; rm -rf /";
// Vulnerable to flag injection
await Bun.$`ls ${userInput}`;
// FIX: Use -- to end arguments
await Bun.$`ls -- ${userInput}`;
Validation
Always validate user input:
function isValidFilename(name: string): boolean {
return /^[a-zA-Z0-9._-]+$/.test(name) && !name.startsWith(".");
}
if (isValidFilename(userInput)) {
await Bun.$`cat ${userInput}`;
} else {
throw new Error("Invalid filename");
}
Best Practices
- Use Bun.$ for shell commands - automatic escaping
- Validate user input before passing to commands
- Use – to prevent flag injection in arguments
- Avoid bash -c - it bypasses escaping
- Use Bun.spawn for complex scenarios with explicit args array
Common Patterns
Running NPM Scripts
await Bun.$`npm run build`;
await Bun.$`npm test`;
Git Operations
const branch = await Bun.$`git rev-parse --abbrev-ref HEAD`.text();
await Bun.$`git add .`;
await Bun.$`git commit -m ${"feat: new feature"}`;
File Operations
// Find files
const files = await Bun.$`find . -name "*.ts"`.text();
// Process files
for (const file of files.trim().split("\n")) {
await Bun.$`prettier --write ${file}`;
}
Anti-patterns
- DON’T use bash -c to bypass escaping
- DON’T pass untrusted input without validation
- DON’T ignore exit codes
- DON’T forget – to prevent flag injection
References
Bun SQLite Basics
Overview
Bun includes a built-in, high-performance SQLite driver (bun:sqlite) with zero dependencies.
Opening a Database
import { Database } from "bun:sqlite";
// Open or create database file
const db = new Database("mydb.sqlite");
// In-memory database
const memDb = new Database(":memory:");
// Read-only database
const readOnlyDb = new Database("mydb.sqlite", { readonly: true });
// Create database with options
const db = new Database("mydb.sqlite", {
create: true, // Create if doesn't exist (default: true)
readonly: false, // Read-only mode (default: false)
strict: true // Enable strict mode (default: false)
});
Basic Queries
Creating Tables
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");
// Create table
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at INTEGER DEFAULT (unixepoch())
)
`);
Inserting Data
// Insert single row
db.run(
"INSERT INTO users (name, email) VALUES (?, ?)",
"Alice",
"alice@example.com"
);
// Get last inserted ID
const result = db.run(
"INSERT INTO users (name, email) VALUES (?, ?)",
"Bob",
"bob@example.com"
);
console.log("Inserted ID:", result.lastInsertRowid);
console.log("Changes:", result.changes);
Querying Data
// Get single row
const user = db.query("SELECT * FROM users WHERE id = ?").get(1);
console.log(user); // { id: 1, name: "Alice", email: "alice@example.com", ... }
// Get all rows
const users = db.query("SELECT * FROM users").all();
console.log(users); // [{ id: 1, ... }, { id: 2, ... }]
// Get specific values only
const emails = db.query("SELECT email FROM users").values();
console.log(emails); // [["alice@example.com"], ["bob@example.com"]]
Updating Data
const result = db.run(
"UPDATE users SET name = ? WHERE id = ?",
"Alice Smith",
1
);
console.log("Rows updated:", result.changes);
Deleting Data
const result = db.run("DELETE FROM users WHERE id = ?", 1);
console.log("Rows deleted:", result.changes);
Query Methods
.run() - Execute without returning rows
// For INSERT, UPDATE, DELETE, CREATE, DROP
const result = db.run("INSERT INTO users (name, email) VALUES (?, ?)", "Alice", "alice@example.com");
// Returns: { changes: number, lastInsertRowid: number }
.query().get() - Get single row
const query = db.query("SELECT * FROM users WHERE id = ?");
const user = query.get(1);
// Returns: object | undefined
.query().all() - Get all rows
const query = db.query("SELECT * FROM users");
const users = query.all();
// Returns: array of objects
.query().values() - Get raw values
const query = db.query("SELECT name, email FROM users");
const values = query.values();
// Returns: [["Alice", "alice@example.com"], ["Bob", "bob@example.com"]]
Parameterized Queries
Positional Parameters (?)
const user = db.query("SELECT * FROM users WHERE id = ?").get(1);
const user2 = db.query("SELECT * FROM users WHERE name = ? AND email = ?")
.get("Alice", "alice@example.com");
Named Parameters
const user = db.query("SELECT * FROM users WHERE id = $id")
.get({ $id: 1 });
const user2 = db.query("SELECT * FROM users WHERE name = $name AND email = $email")
.get({ $name: "Alice", $email: "alice@example.com" });
Closing the Database
db.close();
// Check if closed
console.log(db.closed); // true
Error Handling
import { Database } from "bun:sqlite";
try {
const db = new Database("mydb.sqlite");
db.run("INSERT INTO users (name, email) VALUES (?, ?)", "Alice", "alice@example.com");
db.close();
} catch (error) {
console.error("Database error:", error);
}
Common Patterns
CRUD Operations Class
import { Database } from "bun:sqlite";
class UserRepository {
private db: Database;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.initTable();
}
private initTable() {
this.db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at INTEGER DEFAULT (unixepoch())
)
`);
}
create(name: string, email: string) {
return this.db.run(
"INSERT INTO users (name, email) VALUES (?, ?)",
name,
email
);
}
findById(id: number) {
return this.db.query("SELECT * FROM users WHERE id = ?").get(id);
}
findAll() {
return this.db.query("SELECT * FROM users").all();
}
update(id: number, name: string, email: string) {
return this.db.run(
"UPDATE users SET name = ?, email = ? WHERE id = ?",
name,
email,
id
);
}
delete(id: number) {
return this.db.run("DELETE FROM users WHERE id = ?", id);
}
close() {
this.db.close();
}
}
Checking if Table Exists
const tableExists = db.query(`
SELECT name FROM sqlite_master
WHERE type='table' AND name=?
`).get("users");
if (!tableExists) {
db.run("CREATE TABLE users (...)");
}
Best Practices
- Always use parameterized queries - Prevent SQL injection
- Close database connections - Call
db.close()when done - Check query results -
.get()returnsundefinedif no row found - Handle errors - Wrap database operations in try/catch
- Use appropriate query methods -
.run()for mutations,.query()for selects
Anti-patterns
- ❌ String interpolation for queries (SQL injection risk)
- ❌ Not closing database connections
- ❌ Ignoring query errors
- ❌ Using
.run()for SELECT queries - ❌ Not checking if results exist before accessing
Performance Tips
- Use prepared statements for repeated queries (next section)
- Use transactions for bulk operations (see sqlite-transactions.md)
- Create indexes on frequently queried columns
- Use
EXPLAIN QUERY PLANto optimize slow queries
Bun Test Framework
Overview
Bun includes a built-in test runner (bun:test) that’s fast, zero-configuration, and Jest-compatible.
Basic Test Structure
import { describe, test, expect } from "bun:test";
describe("User authentication", () => {
test("should create new user", () => {
const user = { id: 1, name: "Alice" };
expect(user.id).toBe(1);
expect(user.name).toEqual("Alice");
});
});
Running Tests
# Run all tests
bun test
# Run specific test file
bun test auth.test.ts
# Watch mode
bun test --watch
# Coverage
bun test --coverage
Test Lifecycle Hooks
import { beforeAll, afterAll, beforeEach, afterEach, describe, test } from "bun:test";
describe("Database operations", () => {
beforeAll(() => {
// Setup database connection once
});
afterAll(() => {
// Close database connection
});
beforeEach(() => {
// Clear data before each test
});
afterEach(() => {
// Cleanup after each test
});
test("should insert record", () => {
// Test code
});
});
Async Testing
import { test, expect } from "bun:test";
test("async operations", async () => {
const data = await fetchData();
expect(data).toBeDefined();
});
test("promise rejection", async () => {
await expect(failingPromise()).rejects.toThrow("Error message");
});
Test Organization
// Group related tests
describe("User service", () => {
describe("create()", () => {
test("with valid data", () => { });
test("with invalid data", () => { });
});
describe("delete()", () => {
test("existing user", () => { });
test("non-existent user", () => { });
});
});
Best Practices
- Use Bun’s built-in test runner - No need for Jest, Vitest, or Mocha
- Organize tests with describe/test - Group related tests logically
- Use lifecycle hooks - Setup/teardown with beforeAll/afterAll/beforeEach/afterEach
- Test async code properly - Use async/await and expect().rejects/resolves
- Make tests deterministic - Avoid flaky tests with proper awaiting
Anti-patterns
- ❌ Using external test runners (Bun’s is faster)
- ❌ Forgetting to clean up resources (use afterEach/afterAll)
- ❌ Testing implementation details (test public APIs)
- ❌ Writing flaky tests (ensure deterministic behavior)
Bun Test Matchers
Overview
Bun’s test runner provides Jest-compatible matchers for writing assertions in your tests.
Basic Matchers
import { test, expect } from "bun:test";
test("basic matchers", () => {
// Equality
expect(2 + 2).toBe(4); // Strict equality (===)
expect({ name: "Alice" }).toEqual({ name: "Alice" }); // Deep equality
// Truthiness
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect("value").toBeDefined();
});
Numeric Matchers
test("numeric matchers", () => {
expect(10).toBeGreaterThan(5);
expect(5).toBeLessThan(10);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThanOrEqual(5);
// Floating point comparison
expect(0.1 + 0.2).toBeCloseTo(0.3); // Handles floating point precision
});
String Matchers
test("string matchers", () => {
expect("Hello World").toMatch(/World/); // Regular expression
expect("Hello World").toMatch("World"); // Substring
expect("error").toContain("err"); // Contains substring
});
Array and Object Matchers
test("array matchers", () => {
const array = [1, 2, 3, 4, 5];
expect(array).toHaveLength(5);
expect(array).toContain(3);
expect(array).toContainEqual(3);
});
test("object matchers", () => {
const user = { id: 1, name: "Alice", email: "alice@example.com" };
expect(user).toHaveProperty("name");
expect(user).toHaveProperty("name", "Alice");
expect(user).toMatchObject({ name: "Alice" }); // Partial match
});
Exception Matchers
test("exception matchers", () => {
const throwError = () => {
throw new Error("Something went wrong");
};
expect(throwError).toThrow(); // Any error
expect(throwError).toThrow(Error); // Specific error type
expect(throwError).toThrow("Something went wrong"); // Error message
expect(throwError).toThrow(/went wrong/); // Error message pattern
});
Promise Matchers
test("async matchers", async () => {
const resolves = Promise.resolve("success");
const rejects = Promise.reject(new Error("failed"));
await expect(resolves).resolves.toBe("success");
await expect(rejects).rejects.toThrow("failed");
});
Negation
test("negation with .not", () => {
expect(5).not.toBe(10);
expect("hello").not.toMatch(/world/);
expect([1, 2, 3]).not.toContain(4);
});
Type Matchers
test("type checking", () => {
expect("hello").toBeInstanceOf(String);
expect([1, 2, 3]).toBeInstanceOf(Array);
expect(new Date()).toBeInstanceOf(Date);
});
Common Patterns
Testing API Responses
test("API response structure", async () => {
const response = await fetch("/api/users/1");
const data = await response.json();
expect(response.status).toBe(200);
expect(data).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
email: expect.stringMatching(/@/)
});
});
Testing Arrays
test("array operations", () => {
const items = [1, 2, 3, 4, 5];
expect(items).toHaveLength(5);
expect(items).toEqual(expect.arrayContaining([2, 4]));
expect(items).toEqual([1, 2, 3, 4, 5]); // Exact match
});
Testing Objects
test("object validation", () => {
const user = {
id: 1,
name: "Alice",
settings: { theme: "dark" }
};
expect(user).toMatchObject({
name: "Alice",
settings: { theme: "dark" }
});
expect(user).toEqual({
id: expect.any(Number),
name: expect.any(String),
settings: expect.objectContaining({ theme: "dark" })
});
});
Best Practices
- Use toBe for primitives - Use strict equality for numbers, strings, booleans
- Use toEqual for objects/arrays - Use deep equality for complex types
- Use toBeCloseTo for floats - Handle floating point precision issues
- Use specific matchers - Prefer toHaveLength over .length.toBe
- Use toMatchObject for partial matching - Test only relevant properties
- Use descriptive assertions - Make test failures clear and actionable
Anti-patterns
- ❌ Using
.toBe()for objects (use.toEqual()) - ❌ Testing implementation details (test behavior, not internal state)
- ❌ Writing assertions that are too loose (be specific)
- ❌ Not using
.notfor negative assertions - ❌ Comparing floats with
.toBe()(use.toBeCloseTo())
Bun Test Mocking
Overview
Bun provides built-in mocking capabilities through mock() and mock.module() for testing code with dependencies.
Function Mocking
Basic Mock Functions
import { test, expect, mock } from "bun:test";
test("mock function", () => {
const mockFn = mock(() => "mocked value");
expect(mockFn()).toBe("mocked value");
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
});
Mock with Return Values
test("mock return values", () => {
const mockFn = mock();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
mockFn.mockReturnValueOnce(1)
.mockReturnValueOnce(2)
.mockReturnValue(3);
expect(mockFn()).toBe(1);
expect(mockFn()).toBe(2);
expect(mockFn()).toBe(3);
expect(mockFn()).toBe(3);
});
Mock with Implementation
test("mock implementation", () => {
const mockFn = mock((x: number, y: number) => x + y);
expect(mockFn(2, 3)).toBe(5);
expect(mockFn).toHaveBeenCalledWith(2, 3);
});
Module Mocking
Mock Entire Module
import { test, expect, mock } from "bun:test";
mock.module("./database", () => ({
default: {
query: mock(() => Promise.resolve([{ id: 1, name: "Test" }])),
connect: mock(() => Promise.resolve()),
disconnect: mock(() => Promise.resolve())
}
}));
test("database operations", async () => {
const db = await import("./database");
const results = await db.default.query("SELECT * FROM users");
expect(results).toHaveLength(1);
expect(db.default.query).toHaveBeenCalled();
});
Partial Module Mock
import { test, expect, mock } from "bun:test";
import * as userService from "./user-service";
mock.module("./user-service", () => ({
...userService,
sendEmail: mock(() => Promise.resolve(true)) // Mock only sendEmail
}));
test("user creation without email", async () => {
const { createUser, sendEmail } = await import("./user-service");
await createUser({ name: "Alice", email: "alice@example.com" });
expect(sendEmail).toHaveBeenCalledWith("alice@example.com");
});
Spy Functions
Spying on Object Methods
import { test, expect, spyOn } from "bun:test";
test("spy on method", () => {
const calculator = {
add: (a: number, b: number) => a + b
};
const spy = spyOn(calculator, "add");
calculator.add(2, 3);
expect(spy).toHaveBeenCalledWith(2, 3);
expect(spy).toHaveReturnedWith(5);
});
Spy with Mock Implementation
test("spy with mock implementation", () => {
const logger = {
log: (message: string) => console.log(message)
};
const spy = spyOn(logger, "log").mockImplementation(() => {
// Do nothing instead of logging
});
logger.log("test message");
expect(spy).toHaveBeenCalledWith("test message");
});
Mock Assertions
test("mock assertions", () => {
const mockFn = mock();
mockFn("arg1", "arg2");
mockFn("arg3");
// Called assertions
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
// Argument assertions
expect(mockFn).toHaveBeenCalledWith("arg1", "arg2");
expect(mockFn).toHaveBeenNthCalledWith(1, "arg1", "arg2");
expect(mockFn).toHaveBeenNthCalledWith(2, "arg3");
expect(mockFn).toHaveBeenLastCalledWith("arg3");
});
Resetting Mocks
import { test, expect, mock, beforeEach } from "bun:test";
const mockFn = mock();
beforeEach(() => {
mockFn.mockClear(); // Clear call history
// or
mockFn.mockReset(); // Clear call history and implementation
// or
mockFn.mockRestore(); // Restore original implementation (for spies)
});
test("first test", () => {
mockFn();
expect(mockFn).toHaveBeenCalledTimes(1);
});
test("second test", () => {
// mockFn call count is reset
expect(mockFn).not.toHaveBeenCalled();
});
Common Patterns
Mocking HTTP Fetch
import { test, expect, mock } from "bun:test";
mock.module("global", () => ({
fetch: mock(() => Promise.resolve({
json: () => Promise.resolve({ id: 1, name: "Test" }),
status: 200,
ok: true
}))
}));
test("fetch API data", async () => {
const response = await fetch("/api/users/1");
const data = await response.json();
expect(data).toEqual({ id: 1, name: "Test" });
});
Mocking Timers
import { test, expect, mock } from "bun:test";
test("delayed execution", () => {
const callback = mock();
setTimeout(callback, 1000);
// Fast-forward time
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledTimes(1);
});
Mocking File System
import { test, expect, mock } from "bun:test";
mock.module("node:fs/promises", () => ({
readFile: mock(() => Promise.resolve("file contents")),
writeFile: mock(() => Promise.resolve()),
exists: mock(() => Promise.resolve(true))
}));
test("file operations", async () => {
const fs = await import("node:fs/promises");
const content = await fs.readFile("test.txt", "utf-8");
expect(content).toBe("file contents");
expect(fs.readFile).toHaveBeenCalledWith("test.txt", "utf-8");
});
Best Practices
- Mock at the boundary - Mock external dependencies, not internal logic
- Reset mocks between tests - Use beforeEach to clear mock state
- Verify mock calls - Assert that mocks were called with expected arguments
- Mock minimal scope - Only mock what’s necessary for the test
- Use spies for partial mocking - Spy on real objects when you need partial behavior
Anti-patterns
- ❌ Mocking too much (over-mocking makes tests brittle)
- ❌ Not resetting mocks between tests (causes test interdependence)
- ❌ Mocking implementation details (mock external dependencies only)
- ❌ Complex mock setup (simplify or refactor the code under test)
- ❌ Not asserting mock behavior (verify mocks were called correctly)
Bun Test Snapshots
Overview
Snapshot testing captures the output of a function/component and compares it against stored snapshots on subsequent runs.
Basic Snapshot Testing
import { test, expect } from "bun:test";
test("snapshot test", () => {
const data = {
id: 1,
name: "Alice",
email: "alice@example.com",
createdAt: new Date("2024-01-01")
};
expect(data).toMatchSnapshot();
});
Inline Snapshots
test("inline snapshot", () => {
const user = { id: 1, name: "Bob" };
expect(user).toMatchInlineSnapshot(`
{
"id": 1,
"name": "Bob"
}
`);
});
Updating Snapshots
# Update all snapshots
bun test --update-snapshots
# Or use -u flag
bun test -u
# Update snapshots for specific test file
bun test user.test.ts -u
Snapshot File Structure
Snapshots are stored in __snapshots__ directory:
tests/
user.test.ts
__snapshots__/
user.test.ts.snap
Testing API Responses
import { test, expect } from "bun:test";
test("API response snapshot", async () => {
const response = await fetch("http://localhost:3000/api/users/1");
const data = await response.json();
expect(data).toMatchSnapshot();
});
Testing Rendered Output
import { test, expect } from "bun:test";
import { renderToString } from "some-template-engine";
test("template rendering", () => {
const html = renderToString("user-profile", {
name: "Alice",
email: "alice@example.com"
});
expect(html).toMatchSnapshot();
});
Property Matchers
Use property matchers when some values are dynamic:
test("snapshot with dynamic values", () => {
const user = {
id: Math.random(),
name: "Alice",
createdAt: new Date()
};
expect(user).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(Date)
});
});
Snapshot Serialization
Custom Serializer
import { test, expect } from "bun:test";
expect.addSnapshotSerializer({
test: (val) => val instanceof Date,
serialize: (val) => `Date(${val.toISOString()})`
});
test("custom date serialization", () => {
const event = {
name: "Meeting",
date: new Date("2024-01-01T10:00:00Z")
};
expect(event).toMatchSnapshot();
// Snapshot will show: Date(2024-01-01T10:00:00.000Z)
});
Snapshot Testing Patterns
Testing Error Messages
test("error message snapshot", () => {
const error = validateUser({ email: "invalid" });
expect(error).toMatchSnapshot();
});
Testing Configuration Objects
test("config snapshot", () => {
const config = loadConfig("production");
// Exclude sensitive data
const { apiKey, ...publicConfig } = config;
expect(publicConfig).toMatchSnapshot();
});
Testing Database Queries
test("SQL query snapshot", () => {
const query = buildQuery({
table: "users",
where: { active: true },
orderBy: "created_at",
limit: 10
});
expect(query).toMatchSnapshot();
});
Snapshot Diffing
When a snapshot fails, Bun shows a diff:
Expected:
{
"name": "Alice",
"age": 30
}
Received:
{
"name": "Alice",
"age": 31
}
Best Practices
- Commit snapshots to version control - Track changes over time
- Review snapshot changes carefully - Don’t blindly update snapshots
- Use property matchers for dynamic data - Handle timestamps, IDs, random values
- Keep snapshots small - Large snapshots are hard to review
- Name tests descriptively - Make it clear what’s being snapshotted
- Update intentionally - Only update when changes are expected
Anti-patterns
- ❌ Blindly updating snapshots without review
- ❌ Snapshotting large objects with too much irrelevant data
- ❌ Not using property matchers for dynamic values
- ❌ Snapshotting implementation details instead of outputs
- ❌ Using snapshots as the only test (combine with assertions)
- ❌ Not committing snapshot files to version control
When to Use Snapshots
Good Use Cases
- ✅ Testing API responses
- ✅ Testing rendered HTML/templates
- ✅ Testing generated code
- ✅ Testing configuration objects
- ✅ Testing serialized data structures
Poor Use Cases
- ❌ Testing simple values (use
toBe()ortoEqual()) - ❌ Testing implementation details
- ❌ Testing frequently changing data
- ❌ Testing when precision assertions are better
Snapshot Organization
// Group related snapshots
describe("User API", () => {
test("GET /users/:id", async () => {
const response = await fetch("/users/1");
expect(await response.json()).toMatchSnapshot();
});
test("POST /users", async () => {
const response = await fetch("/users", {
method: "POST",
body: JSON.stringify({ name: "Bob" })
});
expect(await response.json()).toMatchSnapshot();
});
});
Cleaning Up Obsolete Snapshots
# Bun automatically removes obsolete snapshots when tests are deleted
bun test
# Force cleanup
bun test --update-snapshots