| name | bun-runtime-best-practices |
| description | Bun runtime best practices and API usage patterns
When user works with file I/O, environment variables, subprocess spawning, or uses Node.js APIs that have Bun equivalents
|
Bun Runtime Best Practices Agent
What's New in Bun 1.3 (October 2025)
- Unified SQL API: Built-in PostgreSQL, MySQL/MariaDB, and SQLite clients (
Bun.SQL)
- Zero Dependencies: One incredibly fast database library, no external packages needed
- Full-Stack Dev Server: Zero-config frontend development with hot reloading
- Built-in Redis Client: Native Redis support without additional packages
- Standalone Executables: Cross-platform compilation for distribution
- 8x Faster Startup: Compared to Node.js, with 145k req/s HTTP throughput (Node: 65k)
- Isolated Installs: Minimize dependency conflicts across projects
- Vercel Runtime Support: Deploy Bun apps to Vercel seamlessly
Overview
This agent teaches Bun-specific APIs and patterns to replace Node.js equivalents, based on coding standards from scout-for-lol and homelab repositories. Bun provides faster, more modern alternatives to Node.js APIs.
Performance Note: Bun 1.3 delivers 8x faster startup than Node.js with 145k requests/second HTTP server performance. Built-in database clients (PostgreSQL, MySQL, SQLite, Redis) eliminate external dependencies while maintaining zero-config simplicity.
Core Principle
Prefer Bun APIs over Node.js imports for better performance and modern patterns.
File I/O
Use Bun.file() and Bun.write()
❌ Avoid: Node.js fs module
import fs from "fs";
import { promises as fs } from "fs/promises";
import * as fs from "node:fs/promises";
const content = await fs.readFile("file.txt", "utf-8");
await fs.writeFile("file.txt", "content");
✅ Prefer: Bun.file() and Bun.write()
const file = Bun.file("file.txt");
const content = await file.text();
const json = await file.json();
const arrayBuffer = await file.arrayBuffer();
const stream = file.stream();
await Bun.write("output.txt", "Hello, world!");
await Bun.write("data.json", JSON.stringify({ foo: "bar" }));
await Bun.write("binary.dat", new Uint8Array([1, 2, 3]));
await Bun.write("file.txt", "content", {
createPath: true,
});
File Operations
const file = Bun.file("file.txt");
const exists = await file.exists();
const size = file.size;
const type = file.type;
const file = Bun.file("large-file.txt");
for await (const chunk of file.stream()) {
console.log(chunk);
}
Environment Variables
Use Bun.env Instead of process.env
❌ Avoid: process.env
const apiKey = process.env.API_KEY;
const port = process.env.PORT || "3000";
✅ Prefer: Bun.env
const apiKey = Bun.env.API_KEY;
const port = Bun.env.PORT ?? "3000";
Environment Variable Validation
import { z } from "zod";
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.coerce.number().int().positive().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]),
});
const env = EnvSchema.parse(Bun.env);
Process Spawning
Use Bun.spawn() Instead of child_process
❌ Avoid: child_process
import { spawn } from "child_process";
import { exec } from "node:child_process";
const child = spawn("ls", ["-la"]);
✅ Prefer: Bun.spawn()
const proc = Bun.spawn(["ls", "-la"]);
const output = await new Response(proc.stdout).text();
console.log(output);
const proc = Bun.spawn(["git", "status"], {
cwd: "/path/to/repo",
env: { ...Bun.env, GIT_AUTHOR_NAME: "Bot" },
stdout: "pipe",
stderr: "pipe",
});
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
const exitCode = await proc.exited;
Running Shell Commands
const proc = Bun.spawn(["sh", "-c", "echo Hello && date"], {
stdout: "pipe",
});
const output = await new Response(proc.stdout).text();
const proc1 = Bun.spawn(["ls", "-la"], { stdout: "pipe" });
const proc2 = Bun.spawn(["grep", ".ts"], {
stdin: proc1.stdout,
stdout: "pipe",
});
const result = await new Response(proc2.stdout).text();
Path Handling
Use import.meta.dir and import.meta.path
❌ Avoid: path module and __dirname
import path from "path";
import { dirname } from "node:path";
const dir = __dirname;
const file = __filename;
const joined = path.join(__dirname, "config.json");
✅ Prefer: import.meta
const currentDir = import.meta.dir;
const currentFile = import.meta.path;
const configPath = `${import.meta.dir}/config.json`;
const config = Bun.file("./config.json");
Path Utilities
import { resolve } from "path";
const absolutePath = resolve(import.meta.dir, "../config.json");
const configPath = `${import.meta.dir}/../config.json`;
Cryptography
Use Bun.password, Bun.hash(), or Web Crypto API
❌ Avoid: crypto module
import crypto from "crypto";
import { createHash } from "node:crypto";
const hash = crypto.createHash("sha256").update("data").digest("hex");
✅ Prefer: Bun APIs
const hashedPassword = await Bun.password.hash("my-password");
const isValid = await Bun.password.verify("my-password", hashedPassword);
const hashedPassword = await Bun.password.hash("my-password", {
algorithm: "argon2id",
memoryCost: 65536,
timeCost: 3,
});
const hasher = new Bun.CryptoHasher("sha256");
hasher.update("data");
const hash = hasher.digest("hex");
const hash = Bun.hash("data");
const encoder = new TextEncoder();
const data = encoder.encode("data");
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
Binary Data
Prefer Uint8Array Over Buffer
❌ Avoid: Buffer
const buffer = Buffer.from("hello");
const buffer2 = Buffer.alloc(10);
✅ Prefer: Uint8Array and Bun APIs
const encoder = new TextEncoder();
const bytes = encoder.encode("hello");
const file = Bun.file("image.png");
const arrayBuffer = await file.arrayBuffer();
const bytes = new Uint8Array(arrayBuffer);
await Bun.write("output.bin", bytes);
Module System
Use ESM Imports, Never require()
❌ Avoid: CommonJS require
const fs = require("fs");
const { parse } = require("./parser");
✅ Prefer: ESM imports
import fs from "fs";
import { parse } from "./parser.ts";
const module = await import("./dynamic-module.ts");
Import Extensions
Always use .ts extensions in imports:
import { helper } from "./utils/helper.ts";
import type { User } from "./types/user.ts";
import { helper } from "./utils/helper";
import type { User } from "./types/user";
Bun-Specific Features
Bun.sleep()
await Bun.sleep(1000);
await Bun.sleep(100);
Bun.which()
const git = Bun.which("git");
console.log(git);
const nonexistent = Bun.which("nonexistent-command");
console.log(nonexistent);
Bun.peek()
const proc = Bun.spawn(["echo", "hello"], { stdout: "pipe" });
const peeked = await Bun.peek(proc.stdout);
console.log(peeked);
const full = await new Response(proc.stdout).text();
Bun.$.
import { $ } from "bun";
const output = await $`ls -la`.text();
console.log(output);
const result = await $`ls -la | grep .ts`.text();
try {
await $`some-failing-command`;
} catch (error) {
console.error("Command failed:", error);
}
HTTP Server
Use Bun.serve()
Bun.serve({
port: 3000,
fetch(request) {
return new Response("Hello World!");
},
});
Bun.serve({
port: 3000,
fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/api/users") {
return Response.json({ users: [] });
}
if (url.pathname === "/health") {
return new Response("OK");
}
return new Response("Not Found", { status: 404 });
},
});
Bun.serve({
port: 3000,
fetch(request, server) {
if (server.upgrade(request)) {
return;
}
return new Response("HTTP response");
},
websocket: {
message(ws, message) {
ws.send(`Echo: ${message}`);
},
},
});
Testing
Use Bun's Built-in Test Runner
import { test, expect, describe, beforeAll, afterAll } from "bun:test";
describe("User validation", () => {
beforeAll(() => {
});
test("validates email format", () => {
const isValid = validateEmail("test@example.com");
expect(isValid).toBe(true);
});
test("rejects invalid email", () => {
const isValid = validateEmail("invalid");
expect(isValid).toBe(false);
});
afterAll(() => {
});
});
Run tests
bun test
bun test --watch
bun test user.test.ts
bun test --coverage
Database Access (Bun 1.3+)
Unified SQL API - PostgreSQL, MySQL, SQLite
Bun 1.3 provides a unified SQL API (Bun.SQL) for PostgreSQL, MySQL/MariaDB, and SQLite with zero external dependencies:
const pg = await Bun.SQL`postgres://user:pass@localhost:5432/db`;
const users = await pg`SELECT * FROM users WHERE active = ${true}`;
const mysql = await Bun.SQL`mysql://user:pass@localhost:3306/db`;
const posts = await mysql`SELECT * FROM posts LIMIT ${10}`;
const sqlite = await Bun.SQL`sqlite:///path/to/db.sqlite`;
const data = await sqlite`SELECT * FROM table WHERE id = ${123}`;
Benefits:
- Zero dependencies: No pg, mysql2, or better-sqlite3 packages needed
- Tagged template literals: Safe parameterized queries, SQL injection protection
- Promise-based: Modern async/await API throughout
- Connection pooling: Built-in for PostgreSQL and MySQL
- Transactions: Full transaction support across all databases
PostgreSQL Example
const db = await Bun.SQL`postgres://user:pass@localhost:5432/mydb`;
const [newUser] = await db`
INSERT INTO users (name, email)
VALUES (${name}, ${email})
RETURNING *
`;
const users = await db`
SELECT * FROM users
WHERE created_at > ${sinceDate}
ORDER BY created_at DESC
LIMIT ${limit}
`;
await db.transaction(async (tx) => {
await tx`INSERT INTO accounts (user_id, balance) VALUES (${userId}, ${0})`;
await tx`UPDATE users SET has_account = true WHERE id = ${userId}`;
});
const getUser = db.prepare`SELECT * FROM users WHERE id = ${0}`;
const user1 = await getUser(123);
const user2 = await getUser(456);
await db.close();
MySQL / MariaDB Example
const db = await Bun.SQL`mysql://root:password@localhost:3306/testdb`;
await db`
INSERT INTO products (name, price)
VALUES (${productName}, ${price})
`;
const products = await db`
SELECT * FROM products
WHERE category = ${category}
AND price < ${maxPrice}
`;
const values = products.map((p) => [p.name, p.price]);
await db`INSERT INTO products (name, price) VALUES ${values}`;
await db.close();
SQLite Example (bun:sqlite still available)
const db = await Bun.SQL`sqlite:///mydb.sqlite`;
const users = await db`SELECT * FROM users WHERE active = ${true}`;
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
)
`);
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run("Alice", "alice@example.com");
const query = db.prepare("SELECT * FROM users WHERE email = ?");
const user = query.get("alice@example.com");
db.close();
Choose Bun.SQL for:
- Unified API across PostgreSQL/MySQL/SQLite
- Async/promise-based workflows
- Modern tagged template literal syntax
Choose bun:sqlite for:
- Synchronous SQLite operations
- Lower-level control
- Existing code using prepare/run/get patterns
Built-in Redis Client (Bun 1.3+)
const redis = await Bun.redis.connect("redis://localhost:6379");
await redis.set("key", "value");
const value = await redis.get("key");
await redis.hset("user:123", { name: "Alice", email: "alice@example.com" });
const user = await redis.hgetall("user:123");
const subscriber = await Bun.redis.connect("redis://localhost:6379");
await subscriber.subscribe("channel", (message) => {
console.log("Received:", message);
});
const publisher = await Bun.redis.connect("redis://localhost:6379");
await publisher.publish("channel", "Hello!");
await redis.disconnect();
Best Practices Summary
- File I/O: Use
Bun.file() and Bun.write() instead of fs
- Environment: Use
Bun.env instead of process.env
- Processes: Use
Bun.spawn() instead of child_process
- Paths: Use
import.meta.dir and import.meta.path instead of __dirname
- Crypto: Use
Bun.password, Bun.hash(), or Web Crypto API
- Binary: Use
Uint8Array instead of Buffer
- Modules: Use ESM imports with
.ts extensions
- Testing: Use
bun:test for testing
- Databases: Use
Bun.SQL for PostgreSQL/MySQL/SQLite (unified API, zero dependencies)
- Redis: Use built-in
Bun.redis client (no ioredis needed)
- HTTP: Use
Bun.serve() for servers (145k req/s)
Performance Benefits
Bun 1.3 delivers exceptional performance:
- 8x faster startup than Node.js
- 145k requests/second HTTP throughput (vs Node.js 65k req/s)
- Written in Zig: Close to metal performance
- JavaScriptCore engine: Optimized for modern JavaScript
- Zero-copy operations: Minimizes memory allocations
- Built-in database clients: Faster than external packages (pg, mysql2, better-sqlite3)
- No external dependencies: Database and Redis clients built-in
When to Ask for Help
Ask the user for clarification when:
- Legacy Node.js code needs migration strategy
- Performance requirements are critical
- Third-party libraries depend on Node.js APIs
- The project must support both Bun and Node.js runtimes