Skip to main content
Technology & EngineeringBun344 lines

Bun Runtime APIs

Bun-native runtime APIs including Bun.serve(), Bun.file(), Bun.write(), Bun.spawn(), Bun.sleep(), Bun.env, FFI for calling native libraries, built-in SQLite, S3 client, glob, and semver utilities.

Quick Summary30 lines
You are an expert in Bun's native runtime APIs, covering file I/O, process spawning, environment access, foreign function interface, built-in SQLite, and utility modules.

## Key Points

- **Using `fs.readFile` when `Bun.file()` is available**: The Bun-native API is faster and more ergonomic. Use `Bun.file()` for reads and `Bun.write()` for writes.
- **String-interpolating SQL queries**: Always use prepared statements with `db.prepare()`. SQLite injection is just as dangerous as SQL injection in any other database.
- **Loading SQLite from npm when `bun:sqlite` exists**: The built-in module is faster and has zero dependencies. Only use `better-sqlite3` if you need a feature Bun's SQLite does not expose.
- **Using child_process.exec for shell commands**: Prefer `Bun.spawn()` or `Bun.$` for shell scripting. They are faster and have better ergonomics.
- **Forgetting to null-terminate FFI strings**: C functions expect null-terminated strings. Always append `\0` when passing strings via FFI.
- **Blocking the event loop with Bun.sleepSync**: Use `await Bun.sleep()` for async delays. The sync version blocks all other work.

## Quick Example

```typescript
// Sleep for milliseconds
await Bun.sleep(1000); // 1 second

// Synchronous sleep (blocks the thread)
Bun.sleepSync(100);
```

```typescript
import { semver } from "bun";

console.log(semver.satisfies("1.2.3", "^1.0.0")); // true
console.log(semver.satisfies("2.0.0", "^1.0.0")); // false
console.log(semver.order("1.2.3", "1.3.0"));       // -1 (first is less)
```
skilldb get bun-skills/Bun Runtime APIsFull skill: 344 lines
Paste into your CLAUDE.md or agent config

Bun Runtime APIs — Native High-Performance APIs

You are an expert in Bun's native runtime APIs, covering file I/O, process spawning, environment access, foreign function interface, built-in SQLite, and utility modules.

Bun.file() and Bun.write() — Optimized File I/O

Bun provides its own file APIs that are faster than Node's fs module because they use optimized system calls (io_uring on Linux).

// Reading files
const file = Bun.file("data.json");
console.log(file.size);          // file size in bytes
console.log(file.type);          // MIME type: "application/json"

const text = await file.text();       // read as string
const json = await file.json();       // parse as JSON
const buffer = await file.arrayBuffer(); // read as ArrayBuffer
const stream = file.stream();         // ReadableStream

// Check if a file exists
const exists = await Bun.file("maybe.txt").exists();

// Writing files
await Bun.write("output.txt", "Hello, Bun!");
await Bun.write("data.json", JSON.stringify({ key: "value" }));
await Bun.write("copy.txt", Bun.file("original.txt")); // copy
await Bun.write("image.png", await fetch("https://example.com/img.png"));

// Write to stdout
await Bun.write(Bun.stdout, "Hello to stdout\n");

// Write with Response object
await Bun.write("page.html", new Response("<h1>Hi</h1>"));

Bun.file() is lazy -- it does not read the file until you call .text(), .json(), .arrayBuffer(), or .stream(). This means Bun.file("big.csv") is instant and allocates no memory until you consume it.

Bun.serve() — HTTP and WebSocket Server

const server = Bun.serve({
  port: 3000,
  fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === "/api/health") {
      return Response.json({ status: "ok" });
    }
    return new Response("Not Found", { status: 404 });
  },
  error(err) {
    return new Response(`Error: ${err.message}`, { status: 500 });
  },
});

console.log(`Listening on ${server.url}`);

See the bun-http-server skill for detailed HTTP server patterns.

Bun.spawn() — Process Spawning

// Simple command
const proc = Bun.spawn(["ls", "-la"]);
const output = await new Response(proc.stdout).text();
console.log(output);

// With options
const proc2 = Bun.spawn(["grep", "error"], {
  cwd: "/var/log",
  env: { ...Bun.env, CUSTOM: "value" },
  stdin: "pipe",
  stdout: "pipe",
  stderr: "pipe",
});

// Write to stdin
proc2.stdin.write("line with error\nclean line\n");
proc2.stdin.end();

const result = await new Response(proc2.stdout).text();
await proc2.exited; // wait for process to finish
console.log(proc2.exitCode); // 0

// Synchronous spawn
const syncResult = Bun.spawnSync(["echo", "hello"]);
console.log(syncResult.stdout.toString()); // "hello\n"
console.log(syncResult.exitCode); // 0

Shell scripting with Bun.$

Bun provides a tagged template literal for shell commands:

import { $ } from "bun";

// Simple commands
const result = await $`ls -la`.text();

// Interpolation is safe (auto-escaped)
const dir = "my folder";
await $`ls ${dir}`; // properly handles spaces

// Piping
const count = await $`cat access.log | grep 404 | wc -l`.text();

// Quiet mode (suppress stdout)
await $`rm -rf dist`.quiet();

// Get exit code without throwing on failure
const { exitCode } = await $`test -f config.json`.nothrow();

// Environment variables
await $`echo $HOME`;

// Redirect to file
await $`echo "hello" > output.txt`;

Bun.sleep() — Async Delay

// Sleep for milliseconds
await Bun.sleep(1000); // 1 second

// Synchronous sleep (blocks the thread)
Bun.sleepSync(100);

Bun.env — Environment Variables

// Read environment variables (same as process.env but typed)
const port = Bun.env.PORT ?? "3000";
const dbUrl = Bun.env.DATABASE_URL;

// Bun automatically loads .env files in this order:
// 1. .env.local
// 2. .env.development / .env.production (based on NODE_ENV)
// 3. .env

// You can also specify env files explicitly:
// bun --env-file=.env.custom run server.ts

// Type-safe env access
if (!Bun.env.API_KEY) {
  throw new Error("API_KEY is required");
}

FFI — Foreign Function Interface

Call native C/C++/Rust shared libraries directly from JavaScript without writing native addons:

import { dlopen, FFIType, suffix } from "bun:ffi";

// Load a shared library (.so on Linux, .dylib on macOS, .dll on Windows)
const lib = dlopen(`libcalc.${suffix}`, {
  add: {
    args: [FFIType.i32, FFIType.i32],
    returns: FFIType.i32,
  },
  multiply: {
    args: [FFIType.f64, FFIType.f64],
    returns: FFIType.f64,
  },
});

console.log(lib.symbols.add(2, 3));        // 5
console.log(lib.symbols.multiply(2.5, 4)); // 10.0

// Close the library when done
lib.close();

Supported FFI types: bool, i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, ptr, cstring, void.

// Working with pointers and strings
const lib = dlopen("libgreet.so", {
  greet: {
    args: [FFIType.cstring],
    returns: FFIType.cstring,
  },
});

const encoder = new TextEncoder();
const name = encoder.encode("World\0"); // null-terminated
const result = lib.symbols.greet(name);

Built-in SQLite

Bun embeds SQLite directly -- no npm packages needed:

import { Database } from "bun:sqlite";

// Open or create a database
const db = new Database("app.db");

// Create tables
db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
  )
`);

// Prepared statements (always use these -- never interpolate SQL)
const insert = db.prepare(
  "INSERT INTO users (name, email) VALUES ($name, $email)"
);
insert.run({ $name: "Alice", $email: "alice@example.com" });

// Query rows
const select = db.prepare("SELECT * FROM users WHERE name = ?");
const user = select.get("Alice"); // single row
const allUsers = db.prepare("SELECT * FROM users").all(); // all rows

// Transactions
const insertMany = db.transaction((users: { name: string; email: string }[]) => {
  for (const u of users) {
    insert.run({ $name: u.name, $email: u.email });
  }
});

insertMany([
  { name: "Bob", email: "bob@example.com" },
  { name: "Carol", email: "carol@example.com" },
]);

// WAL mode for better concurrent read performance
db.exec("PRAGMA journal_mode = WAL");

// In-memory database
const memDb = new Database(":memory:");

// Close when done
db.close();

S3 Client

Bun includes a built-in S3 client that works with any S3-compatible storage:

import { S3Client } from "bun";

const s3 = new S3Client({
  accessKeyId: Bun.env.AWS_ACCESS_KEY_ID!,
  secretAccessKey: Bun.env.AWS_SECRET_ACCESS_KEY!,
  region: "us-east-1",
  // endpoint: "https://s3.example.com", // for S3-compatible services
});

// Write a file
await s3.write("my-bucket/hello.txt", "Hello from Bun!");

// Read a file
const file = s3.file("my-bucket/data.json");
const data = await file.json();

// Check existence
const exists = await s3.file("my-bucket/maybe.txt").exists();

// Delete
await s3.delete("my-bucket/old-file.txt");

// The S3 file object has the same interface as Bun.file()
const size = s3.file("my-bucket/large.csv").size;
const stream = s3.file("my-bucket/large.csv").stream();

Glob

import { Glob } from "bun";

const glob = new Glob("**/*.ts");

// Scan files matching the pattern
for await (const path of glob.scan({ cwd: "./src" })) {
  console.log(path); // "index.ts", "utils/helpers.ts", etc.
}

// Synchronous scan
const files = Array.from(glob.scanSync({ cwd: "./src" }));

// Test if a string matches a glob pattern
const g = new Glob("*.json");
console.log(g.match("package.json")); // true
console.log(g.match("src/data.json")); // false (no ** prefix)

Semver

import { semver } from "bun";

console.log(semver.satisfies("1.2.3", "^1.0.0")); // true
console.log(semver.satisfies("2.0.0", "^1.0.0")); // false
console.log(semver.order("1.2.3", "1.3.0"));       // -1 (first is less)

Hashing

// Fast hashing built into Bun
const hash = Bun.hash("hello world"); // uint64, very fast (Wyhash)

// Crypto hashing
const sha256 = Bun.CryptoHasher.hash("sha256", "hello");
console.log(Buffer.from(sha256).toString("hex"));

// Streaming hash
const hasher = new Bun.CryptoHasher("sha256");
hasher.update("hello ");
hasher.update("world");
const digest = hasher.digest("hex");

// Password hashing (bcrypt and argon2)
const hashed = await Bun.password.hash("mypassword");
const valid = await Bun.password.verify("mypassword", hashed); // true

Anti-Patterns

  • Using fs.readFile when Bun.file() is available: The Bun-native API is faster and more ergonomic. Use Bun.file() for reads and Bun.write() for writes.
  • String-interpolating SQL queries: Always use prepared statements with db.prepare(). SQLite injection is just as dangerous as SQL injection in any other database.
  • Loading SQLite from npm when bun:sqlite exists: The built-in module is faster and has zero dependencies. Only use better-sqlite3 if you need a feature Bun's SQLite does not expose.
  • Using child_process.exec for shell commands: Prefer Bun.spawn() or Bun.$ for shell scripting. They are faster and have better ergonomics.
  • Forgetting to null-terminate FFI strings: C functions expect null-terminated strings. Always append \0 when passing strings via FFI.
  • Blocking the event loop with Bun.sleepSync: Use await Bun.sleep() for async delays. The sync version blocks all other work.

Install this skill directly: skilldb add bun-skills

Get CLI access →