Bun
Fast JS/TS runtime with native bundling, testing, package management.
When to Use
- Running JS/TS apps that benefit from fast startup and native TS support
- Using Bun’s built-in bundler, test runner, or package manager
- Writing HTTP servers, scripts, or CLI tools
- Don’t use for: full Node.js API compat, native C++ addons, LTS stability
Critical Patterns
✅ REQUIRED: Bun.serve() for HTTP Servers
Built-in HTTP with streaming, no framework needed.
// CORRECT: zero-dependency HTTP with Bun.serve
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === '/health') return Response.json({ ok: true });
return new Response('Not found', { status: 404 });
},
});
// WRONG: importing express just for a simple endpoint
import express from 'express';
✅ REQUIRED: Bun.file() for File I/O
Native file API with lazy BunFile for fast I/O.
// CORRECT: Bun-native file operations
const file = Bun.file('./config.json');
const config = await file.json();
await Bun.write('./output.txt', 'Hello from Bun');
// WRONG: Node fs/promises in a Bun project
import { readFile } from 'fs/promises';
✅ REQUIRED: bun:test for Testing
Jest-compatible test runner, no install/config.
import { test, expect, describe, mock } from 'bun:test';
describe('math utils', () => {
test('adds numbers', () => {
expect(2 + 3).toBe(5);
});
test('mocks fetch', async () => {
const fn = mock(() => Response.json({ ok: true }));
globalThis.fetch = fn;
const res = await fetch('/api');
expect(fn).toHaveBeenCalledTimes(1);
});
});
✅ REQUIRED: bunx for Package Execution
Run npm binaries without install (like npx, faster).
bunx tsc --noEmit
bunx prettier --write src/
bunx drizzle-kit generate
✅ REQUIRED: Workspace Configuration
Monorepo support via npm-style workspaces in package.json.
{
"workspaces": ["packages/*", "apps/*"],
"scripts": {
"dev": "bun run --filter './apps/*' dev",
"test": "bun test --recursive"
}
}
✅ REQUIRED: Bun.spawn() for Shell Commands
Spawn child processes with native API — faster than Node’s child_process.
// CORRECT: Bun-native process spawning
const proc = Bun.spawn(['git', 'status'], {
stdout: 'pipe',
stderr: 'pipe',
});
const output = await new Response(proc.stdout).text();
console.log(output);
// Alternative: Template literal syntax (shell $)
import { $ } from 'bun';
const result = await $`git status`;
console.log(result.stdout.toString());
// WRONG: Node's child_process (slower, requires import)
import { exec } from 'child_process';
exec('git status', (err, stdout) => console.log(stdout));
✅ REQUIRED: Plugin System for Custom Loaders
Extend Bun’s bundler with plugins for custom file types.
import type { BunPlugin } from 'bun';
const myPlugin: BunPlugin = {
name: 'yaml-loader',
setup(build) {
build.onLoad({ filter: /\.yaml$/ }, async (args) => {
const text = await Bun.file(args.path).text();
const yaml = parseYAML(text); // Your YAML parser
return {
contents: `export default ${JSON.stringify(yaml)}`,
loader: 'js',
};
});
},
};
// Use in build
await Bun.build({
entrypoints: ['./index.ts'],
plugins: [myPlugin],
});
✅ REQUIRED: WebSockets with Bun.serve()
Native WebSocket support in HTTP server with zero dependencies.
Bun.serve({
port: 3000,
fetch(req, server) {
const url = new URL(req.url);
if (url.pathname === '/ws') {
if (server.upgrade(req)) return; // Upgrade to WebSocket
return new Response('Upgrade failed', { status: 400 });
}
return new Response('Not found', { status: 404 });
},
websocket: {
message(ws, message) {
console.log('Received:', message);
ws.send(`Echo: ${message}`);
},
open(ws) {
console.log('Client connected');
},
close(ws) {
console.log('Client disconnected');
},
},
});
Decision Tree
Simple HTTP service?
→ Bun.serve() with no framework
Reading/writing files?
→ Bun.file() and Bun.write()
Running tests?
→ bun test (Jest-compatible, zero config)
One-off package binary?
→ bunx <package>
Bundling for production?
→ bun build --target=browser or --target=bun
Monorepo?
→ Configure workspaces in root package.json
Node API not supported?
→ Check compatibility docs; fall back to Node
Shell scripting?
→ Bun.spawn() or Bun.$ tagged template
Example
// server.ts -- HTTP server with route map
const routes: Record<string, (req: Request) => Response | Promise<Response>> = {
'/': () => new Response('Welcome to the Bun server'),
'/health': () => Response.json({ status: 'ok' }),
'/file': async () => {
const file = Bun.file('./data.txt');
if (await file.exists()) return new Response(file);
return new Response('Not found', { status: 404 });
},
};
Bun.serve({
port: Number(Bun.env.PORT) || 3000,
fetch(req) {
const handler = routes[new URL(req.url).pathname];
return handler ? handler(req) : new Response('Not found', { status: 404 });
},
});
Edge Cases
-
Node.js API gaps: Some built-ins (
vm,worker_threads) partial. Check compat docs. -
Native modules: C++ addons may not load. Use
bun:ffior WASM. -
Watch modes:
--watchrestarts process,--hotenables HMR (experimental). -
Environment vars:
Bun.env.VAR_NAMEorprocess.env..envauto-loads. -
Large files:
Bun.file()is lazy. Stream withnew Response(file). -
TS transpilation: On-the-fly, no type-check. Use
bunx tsc --noEmitin CI. -
Package resolution: Aggressive caching. Try
bun install --forceif issues. -
Port conflicts: Bun crashes if port in use. Handle with try/catch.
-
Test isolation: Same process by default. Use
--preloador--bail. -
Build targets:
--target=bun(Bun only),--target=node(Node),--target=browser(client).
Checklist
- Use
Bun.serve()for simple HTTP services instead of frameworks - Use
Bun.file()/Bun.write()instead offs - Tests use
bun:testwith no external runner - Scripts use
bun runinstead ofnpm run - Production builds use
bun buildwith correct--target - Node API compatibility verified for imported modules