Bun: The Complete Guide to the All-in-One JavaScript Runtime
Bun is an all-in-one JavaScript runtime built from the ground up to be fast. Written in Zig and using JavaScriptCore (the engine powering Safari) instead of V8, Bun combines a runtime, package manager, bundler, and test runner into a single executable. Where most JavaScript toolchains require you to wire together Node.js, npm or Yarn, esbuild or Webpack, and Jest or Vitest, Bun handles all of it natively — with benchmarks that consistently show 2-5x faster startup times and package installation speeds that leave npm in the dust. Since Bun 1.0's production-stable release and the continued improvements in 1.1+, it has become a serious choice for everything from small scripts to production web services.
Why Bun Matters for Web Developers
The JavaScript ecosystem has long suffered from toolchain sprawl — a typical project might involve five or more separate tools just to go from source code to running server. Each tool has its own configuration format, its own version to manage, and its own surface area for things to go wrong. Bun collapses this complexity. One binary handles the tasks that previously required an entire dependency tree of dev tooling, which means fewer things to install, fewer things to configure, and fewer things to debug when something breaks in CI.
Raw performance is the other headline. Bun's startup time is dramatically lower than Node.js because JavaScriptCore initialises faster than V8. For serverless functions, scripts, and CLI tools, this matters enormously — cold starts that took 300ms under Node can drop below 50ms with Bun. For bun install, the speed difference is almost embarrassing: a node_modules directory that takes 45 seconds to populate with npm often takes under five seconds with Bun, because it uses a global module cache and hard links rather than copying files.
Native TypeScript support means there is no compilation step to set up before running .ts files. Running bun run server.ts just works, with no ts-node, no tsx, no esbuild wrapper required. The same applies to JSX and TSX. Bun parses and transpiles these formats natively, using the same engine that handles JavaScript, which keeps the toolchain coherent and fast.
Node.js compatibility is a deliberate design goal rather than an afterthought. Bun implements the Node.js API surface — fs, path, http, crypto, child_process, stream, and most others — so the vast majority of npm packages work without modification. The bun: namespace exposes Bun-specific APIs for when you want to reach for native capabilities. This means you can migrate an existing Node.js project incrementally, running it under Bun before gradually adopting Bun-native patterns.
Step 1: System Requirements
Before installing Bun, confirm your environment meets the following requirements.
Supported operating systems:
- macOS (x64 and Apple Silicon, macOS 12+)
- Linux (x64 and ARM64, kernel 5.1+, glibc 2.17+)
- Windows (x64, Windows 10+, experimental as of Bun 1.1)
For production servers, Linux is the primary target and receives the most optimisation work. If you are deploying to a VPS or bare metal server, a modern Ubuntu LTS (22.04 or 24.04) or Debian release is an excellent choice.
Git should already be installed if you are working with any version-controlled project. No other prerequisites are required — Bun ships as a single self-contained binary and brings its own JavaScript engine.
Step 2: Install Bun
macOS and Linux
The official install script downloads the Bun binary, verifies its checksum, and places it in ~/.bun/bin:
curl -fsSL https://bun.sh/install | bash
After the script completes, add Bun to your shell's PATH by following the instructions printed to your terminal. For bash:
echo 'export BUN_INSTALL="$HOME/.bun"' >> ~/.bashrc
echo 'export PATH="$BUN_INSTALL/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
For zsh, replace .bashrc with .zshrc.
Homebrew (macOS)
brew tap oven-sh/bun
brew install bun
npm (global install, any platform)
If you already have Node.js available and want a quick install without the curl script:
npm install -g bun
Windows
On Windows, use PowerShell:
powershell -c "irm bun.sh/install.ps1 | iex"
Note that Windows support is still marked experimental. For production workloads on Windows, running Bun inside WSL2 is the more reliable path.
Verify the installation
bun --version
Upgrading Bun
bun upgrade
Step 3: Project Setup
Initialising a new project
bun init creates a new project with a package.json, a basic tsconfig.json, and an entry file:
mkdir my-bun-project
cd my-bun-project
bun init
For a blank project with no prompts:
bun init -y
Package management
Installing all dependencies from an existing package.json:
bun install
Adding a production dependency:
bun add hono
Adding a development dependency:
bun add -d @types/bun
Running a script from package.json:
bun run build
bun run dev
The lockfile
Bun generates a bun.lockb binary lockfile rather than the text-based package-lock.json or yarn.lock. The binary format is faster to parse and smaller on disk. Commit bun.lockb to version control so that all team members and your CI environment get identical dependency trees.
TypeScript configuration
Bun generates a sensible tsconfig.json on bun init, but here is a solid starting point for a Bun-first project:
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "ESNext",
"target": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowImportingTsExtensions": true,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"types": ["bun-types"]
}
}
Install the type definitions for Bun's built-in APIs:
bun add -d bun-types
Step 4: Configure Bun for Your Workflow
bunfig.toml
bunfig.toml is Bun's configuration file, equivalent to .npmrc combined with parts of Vite's and Jest's config:
[install.scopes]
"@mycompany" = { registry = "https://npm.mycompany.internal", token = "$NPM_TOKEN" }
[install]
registry = "https://registry.npmjs.org"
production = false
[test]
coverage = true
coverageReporter = ["text", "lcov"]
coverageDir = "coverage"
preload = ["./tests/setup.ts"]
[bundle]
outdir = "dist"
target = "browser"
Defining scripts
package.json scripts work exactly as they do under npm:
{
"scripts": {
"dev": "bun run --hot src/index.ts",
"build": "bun build src/index.ts --outdir dist --target node",
"start": "bun run dist/index.js",
"test": "bun test",
"typecheck": "tsc --noEmit",
"lint": "eslint src --ext .ts,.tsx"
}
}
Workspaces
Bun supports npm-style workspaces for monorepos:
{
"name": "my-monorepo",
"workspaces": [
"packages/*",
"apps/*"
]
}
Running bun install from the root installs all workspace packages and symlinks them appropriately. To run a script in a specific workspace:
bun run --filter "./apps/web" dev
Step 5: Core Features
Runtime
The simplest use of Bun is as a drop-in replacement for node:
bun server.js
bun server.ts
Bun supports ES modules (import/export) and CommonJS (require) in the same project without configuration.
Package manager
bun install --frozen-lockfile
bun install --production
bun pm ls --all
Bundler
Bun's bundler produces optimised output for both browsers and Node.js-compatible servers:
bun build src/index.ts --outdir dist --target browser
bun build src/server.ts --outdir dist --target node
bun build src/index.ts --outdir dist --minify
bun build src/cli.ts --compile --outfile my-tool
The --compile flag bundles your code and Bun's runtime into a single binary.
You can also use the bundler programmatically:
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
target: "browser",
minify: true,
splitting: true,
});
if (!result.success) {
console.error("Build failed:", result.logs);
process.exit(1);
}
Test runner
Bun's built-in test runner uses the same API as Jest:
import { expect, test, describe, beforeEach, afterEach } from "bun:test";
describe("user authentication", () => {
test("hashes passwords before storing", async () => {
const hash = await Bun.password.hash("hunter2");
expect(hash).not.toBe("hunter2");
expect(await Bun.password.verify("hunter2", hash)).toBe(true);
});
});
Run all tests:
bun test
bun test --watch
bun test --coverage
Building with Bun? DeployHQ connects your Git repo to any server and deploys automatically when you push — SFTP, SSH, or cloud. Try it free.
Step 6: Advanced Features
Bun.serve — HTTP server
Bun.serve() is Bun's built-in HTTP server, implemented in native code:
const server = Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/") {
return new Response("Hello from Bun!", {
headers: { "Content-Type": "text/plain" },
});
}
if (url.pathname === "/health") {
return Response.json({ status: "ok", uptime: process.uptime() });
}
return new Response("Not Found", { status: 404 });
},
error(err) {
console.error(err);
return new Response("Internal Server Error", { status: 500 });
},
});
console.log(`Listening on http://localhost:${server.port}`);
Bun.serve() also supports WebSockets natively:
Bun.serve({
port: 3000,
fetch(req, server) {
if (server.upgrade(req)) {
return;
}
return new Response("Upgrade required", { status: 426 });
},
websocket: {
open(ws) { console.log("Client connected"); },
message(ws, message) { ws.send(`Echo: ${message}`); },
close(ws) { console.log("Client disconnected"); },
},
});
Built-in SQLite
Bun ships with SQLite as a first-class built-in:
import { Database } from "bun:sqlite";
const db = new Database("myapp.sqlite");
db.exec(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
body TEXT,
created_at INTEGER DEFAULT (unixepoch())
)
`);
const insert = db.prepare(
"INSERT INTO posts (title, body) VALUES ($title, $body)"
);
insert.run({ $title: "My First Post", $body: "Hello, world!" });
const posts = db
.query("SELECT * FROM posts ORDER BY created_at DESC LIMIT ?")
.all(10);
db.close();
Foreign Function Interface (FFI)
Bun can call native code from shared libraries:
import { dlopen, FFIType, suffix } from "bun:ffi";
const { symbols } = dlopen(`libm.${suffix}`, {
sqrt: {
args: [FFIType.double],
returns: FFIType.double,
},
});
console.log(symbols.sqrt(144)); // 12
Hot reloading
bun run --watch src/index.ts # restarts on change
bun run --hot src/index.ts # reloads modules without restart
Step 7: Best Practices
When to use Bun vs Node.js
Use Bun when:
- Starting a new project and want a fast, integrated toolchain
- Startup time matters (CLI tools, serverless functions, scripts)
- You want native TypeScript without a separate compilation step
- You need built-in SQLite without a native addon
Keep Node.js when:
- You depend on packages with Node-specific native addons
- You are in a locked-down CI environment where Bun cannot be installed
- Migration overhead is not justified for the team
Commit the lockfile
Always commit bun.lockb. Use --frozen-lockfile in CI for deterministic installs.
Use Bun.$ for shell scripting
import { $ } from "bun";
const output = await $`ls -la`.text();
const dir = "dist";
await $`rm -rf ${dir} && mkdir ${dir}`;
Step 8: Deploy with DeployHQ
Install Bun on your target server
curl -fsSL https://bun.sh/install | bash
source ~/.bashrc
bun --version
Ensure Bun's binary path is in the system PATH for the deploy user:
export BUN_INSTALL="/home/deploy/.bun"
export PATH="$BUN_INSTALL/bin:$PATH"
Configure your DeployHQ build commands
bun install --frozen-lockfile
bun run build
Serving with Bun.serve
Create an ecosystem.config.js:
module.exports = {
apps: [
{
name: "my-bun-app",
script: "bun",
args: "run start",
interpreter: "none",
env: {
NODE_ENV: "production",
PORT: "3000",
},
},
],
};
Post-deployment command:
pm2 reload ecosystem.config.js --env production
Alternatively, use systemd:
[Unit]
Description=My Bun Application
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/my-bun-app
ExecStart=/home/deploy/.bun/bin/bun run start
Restart=always
RestartSec=10
Environment=NODE_ENV=production
Environment=PORT=3000
[Install]
WantedBy=multi-user.target
Branch environments
mainbranch -> production server,NODE_ENV=productiondevelopbranch -> staging server,NODE_ENV=staging
For production, always use --frozen-lockfile:
bun install --frozen-lockfile
NODE_ENV=production bun run build
Step 9: Troubleshooting
bun: command not found after installation
Source your shell config:
source ~/.bashrc
Verify ~/.bun/bin is in your PATH.
bun install fails with lockfile had changes
Run bun install locally without --frozen-lockfile, commit both package.json and bun.lockb, then push.
A package fails to install or import
Some packages with native addons compile specifically for Node.js. Common replacements:
better-sqlite3->bun:sqlite(built-in)bcrypt->Bun.password(built-in)
TypeScript errors about Bun globals
Ensure bun-types is installed and in your tsconfig.json:
{
"compilerOptions": {
"types": ["bun-types"]
}
}
Port already in use on deployment
pm2 restart my-bun-app
# or
lsof -i :3000
Conclusion
Bun represents a significant shift in what a JavaScript runtime can be. By collapsing the runtime, package manager, bundler, and test runner into a single fast binary — with native TypeScript support and strong Node.js compatibility — it removes a layer of toolchain complexity that has been a constant background cost of JavaScript development for years. Whether you adopt it as a full replacement for your Node.js stack or use it incrementally for its package installation speed alone, Bun rewards the switch with faster iteration cycles and simpler project configuration.
For production deployments, the combination of bun install --frozen-lockfile, bun run build, and a simple process manager like PM2 or systemd gives you a reliable, reproducible deployment pipeline. Pair that with DeployHQ's automatic Git-triggered deployments and you have a workflow that goes from git push to live update with minimal manual intervention.
If you have questions about deploying your Bun projects, reach out to us at support@deployhq.com or find us on Twitter/X.