Bun runtime API reference for TypeScript scripts. Covers Bun.file(), Bun.write(), Bun.$() shell, Bun.spawn(), Bun.Glob, Bun.env, bun:sqlite, Bun.hash, Bun.password, compression, and utilities for file generation, data processing, and scripting.
Bun runs TypeScript natively — no tsc compilation, no ts-node, no build step. Run any .ts file directly with bun file.ts. Use Bun's native APIs instead of Node.js equivalents — they're faster, more ergonomic, and require no additional dependencies.
Critical: In a Bun project (has bun.lock, bun.lockb, bunfig.toml, or @types/bun in devDependencies), always use Bun to run scripts (bun file.ts, not node file.ts) and prefer Bun-native APIs over Node.js equivalents. Mixing runtimes causes subtle bugs and unnecessary retries.
Verified against Bun v1.4.2 (2026-09-05). Features are tagged with the version that
introduced them (v1.4+, v1.4.1+, v1.4.2+). Where a release changed existing behavior, both
behaviors are stated so this skill stays correct on older projects -- check bun --version
before relying on a version-tagged item.
Bun ships its complete documentation inside bun-types, version-matched to the runtime.
In any project with bun-types or @types/bun installed:
node_modules/bun-types/docs/**/*.mdx # full docs, plus ~180 task-shaped guides/
node_modules/bun-types/*.d.ts # richest API surface (bun.d.ts, serve.d.ts, sql.d.ts)
node_modules/bun-types/CLAUDE.md # Bun's own agent rules
Consult them before writing non-trivial Bun code. This skill covers which API to reach for; the shipped docs cover exact signatures and options.
bun --version against node_modules/bun-types/package.json.
bun init installs @types/bun@latest, which lags behind the runtime -- correct it with
bun add -d bun-types@<runtime-version>.node_modules/bun-types
is a symlink: find node_modules -name '*.mdx' and rg <pattern> node_modules return
nothing, while find node_modules/bun-types/docs -name '*.mdx' works.node_modules/. Under the global store, every project on the
machine shares the same inode -- a write there hits all of them. Use bun patch.docs/runtime/sql.mdx is
https://bun.com/docs/runtime/sql.| Task | Doc path (under node_modules/bun-types/docs/) |
|---|---|
| HTTP server, routes, WebSockets | runtime/http/server.mdx, runtime/http/routing.mdx, runtime/http/websockets.mdx |
| fetch, TCP, UDP, DNS | runtime/networking/fetch.mdx, runtime/networking/tcp.mdx, runtime/networking/udp.mdx, runtime/networking/dns.mdx |
| File I/O, streams, binary data | runtime/file-io.mdx, runtime/streams.mdx, runtime/binary-data.mdx |
| Shell, subprocesses, PTY | runtime/shell.mdx, runtime/child-process.mdx |
| SQL, SQLite, Redis, S3 | runtime/sql.mdx, runtime/sqlite.mdx, runtime/redis.mdx, runtime/s3.mdx |
| Parsers | runtime/json5.mdx, runtime/jsonl.mdx, runtime/xml.mdx, runtime/toml.mdx, runtime/yaml.mdx, runtime/markdown.mdx, runtime/file-types.mdx |
| Images, WebView, cron, secrets, archives | runtime/image.mdx, runtime/webview.mdx, runtime/cron.mdx, runtime/secrets.mdx, runtime/archive.mdx |
| Hashing, utils, semver, glob, cookies, CSRF | runtime/hashing.mdx, runtime/utils.mdx, runtime/semver.mdx, runtime/glob.mdx, runtime/cookies.mdx, runtime/csrf.mdx |
| Node.js compatibility | runtime/nodejs-compat.mdx |
bun:sqlite)DATABASE_URL in .env or environment (PostgreSQL, MySQL, SQLite via Bun.sql())AWS_ACCESS_KEY_ID or uses S3-compatible storage (Bun.s3)REDIS_URL or VALKEY_URL (Bun.redis)Built-in HTTP server — replaces Express, Fastify, or http.createServer.
Prefer routes over hand-rolled URL parsing -- it gives you params, per-method handlers,
and zero-allocation static responses. fetch is the fallback for unmatched requests.
const server = Bun.serve({
port: 3000,
routes: {
'/health': new Response('OK'), // static, zero-allocation
'/api/users/:id': req => Response.json({ id: req.params.id }),
'/api/posts': { // per-method handlers
GET: () => Response.json(listPosts()),
POST: async req => Response.json(await req.json()),
},
'/static/*': { dir: './public' }, // serve a directory (v1.4+)
},
fetch(req: Request): Response | Promise<Response> { // unmatched requests
return new Response('Not Found', { status: 404 })
},
error(error: Error): Response {
return new Response(`Error: ${error.message}`, { status: 500 })
},
})
console.log(`Listening on ${server.url}`)
Route precedence: exact > :param > * > global /*. A registered '/*' route catches
every unmatched path, so fetch only runs when no '/*' route exists -- use one or the
other as the fallback, not both. Handlers receive a BunRequest (a Request plus params
and cookies).
Key methods: server.stop(), server.reload() (hot-swap handler), server.requestIP(req), server.upgrade(req) (WebSocket).
HTTP/2 (v1.4.1+, experimental). http2: true serves HTTP/2 and HTTP/1.1 on one port with
the same routes and fetch: ALPN picks the protocol over TLS, and a cleartext connection
that opens with the HTTP/2 preface (curl --http2-prior-knowledge, node:http2) gets HTTP/2.
http1: false refuses HTTP/1.x clients. server.upgrade() (WebSockets) and response
trailers are HTTP/1.1-only, so gRPC does not work over it yet.
Reference: See
references/http-server.mdfor TLS, WebSocket upgrade, streaming responses, static file serving, and 1.4 behavior changes. Full API innode_modules/bun-types/docs/runtime/http/server.mdxandruntime/http/routing.mdx.
Raw sockets for non-HTTP protocols -- Bun.listen() / Bun.connect() for TCP, Bun.udpSocket() for UDP, plus the built-in WebSocket client and fetch().
const server = Bun.listen({
hostname: '127.0.0.1',
port: 8080,
socket: {
open(socket) { socket.write('welcome\n') },
data(socket, data) { /* Buffer */ },
},
})
Reference: See
references/networking.mdfor TCP/UDP handlers, Unix sockets, the WebSocket client (ws+unix://), andfetch()transport options (HTTP/2, HTTP/3, proxies, system CA).
// Create a BunFile reference (lazy, no read yet)
const file = Bun.file('path/to/file.txt')
// Read contents
const text = await file.text() // string
const json = await file.json() // parsed JSON
const bytes = await file.arrayBuffer() // ArrayBuffer
const stream = file.stream() // ReadableStream
const blob = await file.blob() // Blob
// File metadata
file.size // Size in bytes
file.type // MIME type (auto-detected)
file.name // File path
await file.exists() // Boolean
// Read from URL
const remote = Bun.file('https://example.com/data.json')
// Write string
await Bun.write('output.txt', 'content')
// Write from BunFile (efficient copy)
await Bun.write('copy.txt', Bun.file('original.txt'))
// Write JSON
await Bun.write('data.json', JSON.stringify(data, null, 2))
// Write Uint8Array / ArrayBuffer
await Bun.write('binary.dat', new Uint8Array([1, 2, 3]))
// Write a Response body -- streamed to disk (v1.4.1+; the whole body was buffered before)
await Bun.write('page.html', await fetch('https://example.com'))
// Write to stdout
await Bun.write(Bun.stdout, 'Hello\n')
Bun.stdin // BunFile for stdin
Bun.stdout // BunFile for stdout
Bun.stderr // BunFile for stderr
// Read all of stdin
const input = await Bun.stdin.text()
// Stream stdin line by line
for await (const chunk of Bun.stdin.stream()) {
// process chunk (Uint8Array)
}
// JSON transform
const data = await Bun.file('input.json').json()
data.version = '2.0.0'
await Bun.write('output.json', JSON.stringify(data, null, 2))
// File generation from template
const template = await Bun.file('template.html').text()
const output = template.replace('{{title}}', 'My Page')
await Bun.write('index.html', output)
// Check if file exists before reading
const file = Bun.file('config.json')
if (await file.exists()) {
const config = await file.json()
}
Reference: See
references/file-io.mdfor BunFile interface, write overloads, streaming, MIME detection, and file watching.
The primary way to run shell commands. Returns a promise with output.
import { $ } from 'bun'
// Basic execution
const result = await $`ls -la`
console.log(result.text()) // stdout as string
// With interpolation (auto-escaped)
const dir = 'my folder'
await $`ls ${dir}` // Safe: "my folder" is properly quoted
// Output methods
const output = await $`echo hello`
output.text() // "hello\n"
output.json() // Parse stdout as JSON
output.lines() // string[] (splits on newlines)
output.bytes() // Uint8Array
output.blob() // Blob
output.exitCode // number
output.stderr // Buffer
// Piping
await $`cat file.txt | grep pattern | wc -l`
// Quiet mode (suppress stdout)
await $`npm install`.quiet()
// No-throw mode (don't throw on non-zero exit)
const result = await $`command-that-might-fail`.nothrow()
if (result.exitCode !== 0) {
console.error('Failed:', result.stderr.toString())
}
// Combined
await $`risky-command`.quiet().nothrow()
// Environment variables
await $`echo $HOME`.env({ HOME: '/custom' })
// Working directory
await $`ls`.cwd('/tmp')
// Redirect to file
await $`echo hello > output.txt`
await $`cat < input.txt`
// Pipe between commands
const input = Buffer.from('hello')
await $`cat`.stdin(input)
For more control over process execution.
const proc = Bun.spawn(['command', 'arg1', 'arg2'], {
cwd: '/path',
env: { ...process.env, CUSTOM: 'value' },
stdin: 'pipe', // 'pipe' | 'inherit' | 'ignore' | BunFile | Blob | Response
stdout: 'pipe', // 'pipe' | 'inherit' | 'ignore' | BunFile
stderr: 'pipe', // 'pipe' | 'inherit' | 'ignore' | BunFile
onExit(proc, exitCode, signalCode, error) {
// Called when process exits
},
})
// Write to stdin
proc.stdin.write('input data')
proc.stdin.end()
// Read stdout
const output = await new Response(proc.stdout).text()
// Wait for completion
await proc.exited // Promise<number> (exit code)
// Kill
proc.kill() // SIGTERM
proc.kill('SIGKILL') // Specific signal
const result = Bun.spawnSync(['command', 'arg1'], {
cwd: '/path',
env: { ...process.env },
})
result.exitCode // number
result.stdout // Buffer
result.stderr // Buffer
result.success // boolean
Reference: See
references/shell-and-process.mdfor complete $ API, spawn options, IPC, and signal handling.
const glob = new Bun.Glob('**/*.ts')
// Async iteration
for await (const path of glob.scan({ cwd: './src', onlyFiles: true })) {
console.log(path)
}
// Sync iteration
for (const path of glob.scanSync('./src')) {
console.log(path)
}
// Test if a path matches
glob.match('src/index.ts') // true
glob.match('README.md') // false
// Scan options
glob.scan({
cwd: './src', // Directory to scan (default: '.')
dot: false, // Include dotfiles (default: false)
onlyFiles: true, // Skip directories (default: true)
absolute: false, // Return absolute paths (default: false)
followSymlinks: false, // Follow symlinks (default: false)
})
Bun.env.NODE_ENV // Environment variable (same as process.env)
Bun.env.DATABASE_URL // Typed access
Bun.argv // string[] — [bunPath, scriptPath, ...args]
// Equivalent: process.argv
Bun.main // Absolute path to the entry point script
import.meta.dir // Directory of current file
import.meta.file // Filename of current file
import.meta.path // Full path of current file
import.meta.dirname // Same as import.meta.dir (Node.js compat)
import.meta.filename // Same as import.meta.path (Node.js compat)
Built-in SQL client for querying databases via connection URL. Zero dependencies, tagged template literals, automatic prepared statements, connection pooling. Use when the project has DATABASE_URL in .env or environment.
import { sql, SQL } from "bun"
// Default instance -- auto-connects using DATABASE_URL from environment
const users = await sql`SELECT * FROM users WHERE active = ${true} LIMIT ${10}`
// Explicit connection
const db = new SQL("postgres://user:pass@localhost:5432/mydb")
const results = await db`SELECT * FROM users`
// MySQL
const mysql = new SQL("mysql://user:pass@localhost:3306/mydb")
const user = { name: "Alice", email: "alice@example.com" }
// Insert -- expands object to (column1, column2) VALUES (val1, val2)
const [newUser] = await sql`INSERT INTO users ${sql(user)} RETURNING *`
// Bulk insert
await sql`INSERT INTO users ${sql([user1, user2, user3])}`
// Update -- expands to SET column1 = val1, column2 = val2
await sql`UPDATE users SET ${sql(updates)} WHERE id = ${userId}`
await sql.begin(async (tx) => {
const [user] = await tx`INSERT INTO users (name) VALUES (${"Alice"}) RETURNING *`
await tx`INSERT INTO audit_log (action, user_id) VALUES ('created', ${user.id})`
})
// Auto-committed on success, rolled back on error
Reference: See
references/sql-client.mdfor connection options, pool management, savepoints, MySQL specifics, and prepared statement configuration.
Built-in S3 client with Web standard Blob API. Zero dependencies, works with any S3-compatible service (AWS S3, Cloudflare R2, MinIO, etc.). Use when the project has AWS_ACCESS_KEY_ID or S3-compatible credentials in environment.
import { s3, write } from "bun"
// Reads credentials from AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, etc.
const file = s3.file("data.json") // Lazy reference, no network yet
// Read from S3
const data = await file.json() // Download and parse JSON
const text = await file.text() // Download as string
const stream = file.stream() // ReadableStream
// Upload to S3
await write(s3.file("output.json"), JSON.stringify(data))
// Presigned URLs (synchronous, no network request)
const url = s3.presign("report.pdf", {
expiresIn: 3600, // 1 hour
method: "PUT", // For uploads
acl: "public-read",
})
// Delete
await file.delete()
Reference: See
references/s3-client.mdfor custom S3Client, presign options, multipart upload, and serving from Bun.serve.
Built-in Redis/Valkey client with zero dependencies. Use when the project has REDIS_URL or VALKEY_URL in environment.
import { redis, RedisClient } from "bun"
// Default client -- reads REDIS_URL from environment
await redis.set("key", "value")
const value = await redis.get("key") // "value" | null
// With expiration
await redis.set("session", "data", "EX", 3600)
// Counter operations
await redis.incr("counter")
await redis.incrby("counter", 5)
// Hash operations
await redis.hset("user:1", "name", "Alice", "email", "alice@example.com")
await redis.hget("user:1", "name") // "Alice"
// Custom client
const client = new RedisClient("redis://user:pass@host:6379")
Reference: See
references/redis-client.mdfor all commands (strings, hashes, lists, sets, sorted sets), pub/sub, pipelines, and common patterns.
Create and extract tarballs with optional gzip compression.
// Create archive
const archive = new Bun.Archive({
"hello.txt": "Hello, World!",
"config.json": JSON.stringify({ key: "value" }),
})
await Bun.write("archive.tar", archive)
// With gzip compression -- write the BYTES, not the Archive (see gotcha below)
const compressed = new Bun.Archive(
{ "hello.txt": "Hello, World!" },
{ compress: "gzip", level: 9 } // level 1-12, default 6
)
await Bun.write("archive.tar.gz", await compressed.bytes())
// Extract (auto-detects gzip)
const tarball = await Bun.file("archive.tar.gz").bytes()
const extracted = new Bun.Archive(tarball)
await extracted.extract("./out") // -> number of entries
await extracted.extract("./out", { glob: ["src/**", "!**/*.test.ts"] })
const files = await extracted.files() // -> Map<string, File>
Gotcha (verified on v1.4.0 through v1.4.2): Bun.write(path, archive) ignores the constructor's
compress option and writes an uncompressed tar under your .tar.gz filename. Bun's own
docs show Bun.write(path, archive) as compressing -- it does not. Always pass
await archive.bytes() (or await archive.blob()), which do honor compress
(tracked upstream: oven-sh/bun#30234).
An Archive is not iterable -- for (const [name, contents] of archive) throws.
Use await archive.files() for a Map<string, File>, or await archive.extract(dir).
Reference:
node_modules/bun-types/docs/runtime/archive.mdx
Parse JSON with comments and trailing commas -- replaces jsonc-parser or json5 packages.
import { JSONC } from "bun"
const config = JSONC.parse(`{
// Database config
"host": "localhost",
"port": 5432, // default port
}`)
Bun automatically uses JSONC parsing for tsconfig.json, jsconfig.json, package.json, and bun.lock. .jsonc files can be imported directly: import config from "./config.jsonc".
import { JSON5, JSONL, XML, TOML, markdown, cron, secrets } from "bun"
// JSON5 -- superset of JSON (comments, unquoted keys, trailing commas)
const config = JSON5.parse(`{ unquoted: 'value', /* comment */ }`)
// JSONL -- newline-delimited JSON
const records = JSONL.parse('{"a":1}\n{"a":2}\n')
JSONL.parseChunk(partial) // { values, read, done, error } for streams
// XML -- SIMD parser + serializer (v1.4+), replaces fast-xml-parser / xml2js
const order = XML.parse('<order id="A1"><item>Tea</item></order>')
// { order: { "@id": "A1", item: "Tea" } } -- @attr / #text convention, values are strings
XML.parse(doc, { compact: false }) // { name, attributes, children } document tree
// TOML -- rewritten for TOML v1.1.0; stringify() added in v1.4
const cfg = TOML.parse('name = "app"')
TOML.stringify({ name: "app" })
// Markdown -- built-in CommonMark + GFM parser (replaces marked, remark, etc.)
const html = markdown.html("# Title\n\n**Bold** text.")
const ansi = markdown.ansi("# Title") // ANSI terminal output (v1.3.12+)
markdown.react(readme) // React elements (v1.3.12+)
markdown.render(src, { heading: (c, { level }) => `<h${level}>${c}</h${level}>` })
// Cron -- OS-level jobs, in-process scheduler, and expression parser
const job = cron("0 9 * * 1-5", runReport) // in-process (v1.3.12+)
const next = cron.parse("0 9 * * 1-5") // -> Date | null (NOT a string)
// Secrets -- OS credential store: Keychain / libsecret / Credential Manager (experimental)
await secrets.set({ service: "my-cli", name: "token", value: t })
const token = await secrets.get({ service: "my-cli", name: "token" }) // string | null
// ANSI-aware string utilities (replace wrap-ansi, slice-ansi npm packages)
const coloredText = "\x1b[31mHello, World!\x1b[0m"
Bun.wrapAnsi(coloredText, 80) // Wrap to column width
Bun.sliceAnsi(coloredText, 0, 5) // Grapheme-aware slice
Changed in 1.4 -- Bun.cron time zone. cron.parse() and the in-process
cron(schedule, handler) read schedules in the process's local time zone. Before 1.4
they used UTC. Pass { tz: "UTC" } as the final argument to restore the old behavior:
cron("0 9 * * *", handler, { tz: "UTC" })
cron.parse("0 9 * * *", Date.now(), { tz: "UTC" })
cron.parse() returns a Date, or null when the expression has no match within 8 years
(e.g. February 30th). cron.remove(title) takes the string title of an OS-level job --
it does not accept a job handle. Stop an in-process job with job.stop() or using.
Changed in 1.4 -- stricter parsers. TOML.parse() and bunfig.toml now throw
SyntaxError on unquoted string values, missing newlines between pairs, and integers past
Number.MAX_SAFE_INTEGER. JSONC.parse() throws SyntaxError on invalid input and on ""
(it returned {} before). YAML.parse() follows YAML 1.2, so yes/no/on/off are
strings, not booleans -- an on: key in a GitHub Actions workflow parses as "on".
Reference: See
references/utilities.mdfor full details on all parsing and utility APIs.
Built-in SQLite3 with zero dependencies. For embedded/local databases -- file-based or in-memory.
import { Database } from 'bun:sqlite'
// Open database
const db = new Database('mydb.sqlite')
const db = new Database(':memory:') // In-memory
// Enable WAL mode (recommended)
db.exec('PRAGMA journal_mode = WAL')
// Execute statements
db.exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
// Prepared statements
const insert = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)')
insert.run('Alice', 'alice@example.com')
// Query
const select = db.prepare('SELECT * FROM users WHERE name = ?')
const user = select.get('Alice') // Single
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer