Bun 2.0 Complete Guide: The All-in-One JavaScript Runtime for 2026

One binary to rule them all. Bun 2.0 is not just another JavaScript runtime — it is a complete toolkit that replaces Node.js, npm, webpack, and Jest with a single, blazing-fast binary written in Zig. In this hands-on guide, you will explore every major feature of Bun and build a production-ready REST API from scratch.
What You Will Learn
By the end of this guide, you will:
- Install and configure Bun 2.0 as your primary JavaScript runtime
- Use the Bun package manager to install dependencies faster than npm, yarn, or pnpm
- Leverage the built-in TypeScript and JSX support with zero configuration
- Build and optimize projects with the Bun bundler
- Write and run tests with the built-in Bun test runner
- Use Bun APIs — file I/O, HTTP server, SQLite, WebSocket, and more
- Build a complete REST API with Bun's native HTTP server
- Deploy a Bun application to production
Prerequisites
Before starting, ensure you have:
- Basic JavaScript/TypeScript knowledge (functions, async/await, modules)
- Terminal familiarity (running commands, navigating directories)
- macOS, Linux, or WSL (Bun supports all three)
- A code editor — VS Code or Cursor recommended
Why Bun?
The JavaScript ecosystem has traditionally required multiple tools for different tasks: Node.js for running code, npm for packages, webpack or esbuild for bundling, and Jest or Vitest for testing. Bun collapses all of these into a single binary:
| Feature | Traditional Stack | Bun |
|---|---|---|
| Runtime | Node.js | Bun |
| Package Manager | npm / yarn / pnpm | bun install |
| Bundler | webpack / esbuild / Rollup | bun build |
| Test Runner | Jest / Vitest | bun test |
| TypeScript | tsc + ts-node | Native support |
| .env loading | dotenv package | Built-in |
Bun achieves this speed through its Zig-based core, JavaScriptCore engine (from Safari), and aggressive system-level optimizations. Package installs that take 30 seconds with npm finish in under 2 seconds with Bun.
Step 1: Installing Bun
Install Bun with a single command:
curl -fsSL https://bun.sh/install | bashOn macOS, you can also use Homebrew:
brew install oven-sh/bun/bunVerify the installation:
bun --version
# 2.x.xBun installs to ~/.bun/bin/bun and automatically adds itself to your PATH.
Updating Bun
Keep Bun up to date with:
bun upgradeStep 2: Your First Bun Project
Create a new project using bun init:
mkdir bun-demo && cd bun-demo
bun initThis generates a minimal project structure:
bun-demo/
├── index.ts
├── package.json
├── tsconfig.json
└── README.md
Notice that Bun defaults to TypeScript — no extra configuration needed. Run the file:
bun run index.ts
# Hello via Bun!There is no compilation step. Bun transpiles TypeScript on the fly with near-zero overhead.
Step 3: Bun as a Package Manager
The Bun package manager is a drop-in replacement for npm, yarn, and pnpm. It reads the same package.json and generates a compatible node_modules folder.
Installing Dependencies
# Install all dependencies from package.json
bun install
# Add a dependency
bun add zod
# Add a dev dependency
bun add -d @types/node
# Remove a dependency
bun remove zodSpeed Comparison
Bun uses a global module cache and hardlinks instead of copying files. A typical bun install on a Next.js project completes in under 2 seconds, compared to 15-30 seconds with npm.
Lockfile
Bun generates a binary lockfile called bun.lockb. It is deterministic and much faster to parse than package-lock.json. If you need a text-based lockfile for code review, you can generate one:
bun install --yarn
# Generates yarn.lock alongside bun.lockbWorkspaces
Bun supports npm workspaces natively:
{
"name": "monorepo",
"workspaces": ["packages/*", "apps/*"]
}# Install all workspace dependencies
bun install
# Run a script in a specific workspace
bun run --filter @myorg/api startStep 4: Built-in TypeScript and JSX
Bun executes .ts, .tsx, .js, and .jsx files directly without any build step or configuration. The built-in transpiler handles:
- TypeScript with full type syntax support
- JSX/TSX transformation
- Decorators (both TC39 and legacy)
- Top-level await
- ES modules and CommonJS interop
Create a file types-demo.ts:
interface User {
id: number;
name: string;
email: string;
}
function greet(user: User): string {
return `Hello, ${user.name}! Your email is ${user.email}.`;
}
const user: User = {
id: 1,
name: "Ahmed",
email: "ahmed@example.com",
};
console.log(greet(user));Run it directly:
bun run types-demo.ts
# Hello, Ahmed! Your email is ahmed@example.com.No tsc, no ts-node, no tsx — just Bun.
Step 5: Bun APIs — The Built-in Standard Library
Bun provides a rich set of built-in APIs that replace common npm packages.
5.1: File I/O with Bun.file()
// Write a file
await Bun.write("output.txt", "Hello from Bun!");
// Read a file
const file = Bun.file("output.txt");
const text = await file.text();
console.log(text); // "Hello from Bun!"
// Get file metadata
console.log(file.size); // bytes
console.log(file.type); // MIME typeBun.file() is lazy — it does not read the file until you call .text(), .json(), .arrayBuffer(), or .stream().
5.2: Environment Variables
Bun automatically loads .env files without any package:
# .env
DATABASE_URL=postgres://localhost:5432/mydb
API_KEY=secret123// Access directly — no dotenv import needed
console.log(Bun.env.DATABASE_URL);
console.log(Bun.env.API_KEY);5.3: Password Hashing
// Hash a password (bcrypt under the hood)
const hash = await Bun.password.hash("my-secure-password");
// Verify a password
const isValid = await Bun.password.verify("my-secure-password", hash);
console.log(isValid); // true5.4: Built-in SQLite
Bun ships with a native SQLite driver — no npm package required:
import { Database } from "bun:sqlite";
const db = new Database("myapp.db");
// Create a table
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 (datetime('now'))
)
`);
// Insert data
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run("Ahmed", "ahmed@example.com");
insert.run("Sara", "sara@example.com");
// Query data
const users = db.query("SELECT * FROM users").all();
console.log(users);5.5: Hashing and Cryptography
// Fast hashing
const hash = Bun.hash("hello world");
console.log(hash);
// Crypto hashing
const hasher = new Bun.CryptoHasher("sha256");
hasher.update("hello world");
console.log(hasher.digest("hex"));Step 6: Building an HTTP Server
Bun's native HTTP server is one of its standout features — it handles hundreds of thousands of requests per second.
Basic Server
// server.ts
Bun.serve({
port: 3000,
fetch(req) {
return new Response("Hello from Bun!");
},
});
console.log("Server running at http://localhost:3000");bun run server.tsThe fetch handler uses the standard Web API Request and Response objects, making your code portable across Bun, Deno, and Cloudflare Workers.
Routing
Build a simple router using URL pattern matching:
// router.ts
Bun.serve({
port: 3000,
fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/" && req.method === "GET") {
return Response.json({ message: "Welcome to the API" });
}
if (url.pathname === "/health" && req.method === "GET") {
return Response.json({ status: "ok", uptime: process.uptime() });
}
return new Response("Not Found", { status: 404 });
},
});Handling JSON Bodies
Bun.serve({
port: 3000,
async fetch(req) {
if (req.method === "POST" && new URL(req.url).pathname === "/users") {
const body = await req.json();
// Process the body...
return Response.json(
{ message: "User created", user: body },
{ status: 201 }
);
}
return new Response("Not Found", { status: 404 });
},
});Step 7: Building a Complete REST API
Let us combine everything into a production-quality REST API — a task manager with SQLite persistence.
Project Structure
bun-tasks-api/
├── src/
│ ├── index.ts # Entry point
│ ├── router.ts # Route handler
│ ├── db.ts # Database setup
│ ├── handlers/
│ │ └── tasks.ts # Task CRUD handlers
│ └── types.ts # Type definitions
├── tests/
│ └── tasks.test.ts # API tests
├── package.json
└── tsconfig.json
Initialize the project
mkdir bun-tasks-api && cd bun-tasks-api
bun init
mkdir -p src/handlers tests7.1: Type Definitions
// src/types.ts
export interface Task {
id: number;
title: string;
description: string | null;
completed: boolean;
created_at: string;
updated_at: string;
}
export interface CreateTaskInput {
title: string;
description?: string;
}
export interface UpdateTaskInput {
title?: string;
description?: string;
completed?: boolean;
}7.2: Database Layer
// src/db.ts
import { Database } from "bun:sqlite";
const db = new Database("tasks.db");
// Enable WAL mode for better concurrent performance
db.run("PRAGMA journal_mode = WAL");
// Create tables
db.run(`
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
)
`);
export default db;7.3: Task Handlers
// src/handlers/tasks.ts
import db from "../db";
import type { CreateTaskInput, UpdateTaskInput, Task } from "../types";
export function getAllTasks(): Task[] {
return db.query("SELECT * FROM tasks ORDER BY created_at DESC").all() as Task[];
}
export function getTaskById(id: number): Task | null {
return db.query("SELECT * FROM tasks WHERE id = ?").get(id) as Task | null;
}
export function createTask(input: CreateTaskInput): Task {
const stmt = db.prepare(
"INSERT INTO tasks (title, description) VALUES (?, ?) RETURNING *"
);
return stmt.get(input.title, input.description ?? null) as Task;
}
export function updateTask(id: number, input: UpdateTaskInput): Task | null {
const existing = getTaskById(id);
if (!existing) return null;
const title = input.title ?? existing.title;
const description = input.description ?? existing.description;
const completed = input.completed !== undefined
? (input.completed ? 1 : 0)
: existing.completed;
const stmt = db.prepare(`
UPDATE tasks
SET title = ?, description = ?, completed = ?, updated_at = datetime('now')
WHERE id = ?
RETURNING *
`);
return stmt.get(title, description, completed, id) as Task;
}
export function deleteTask(id: number): boolean {
const result = db.run("DELETE FROM tasks WHERE id = ?", [id]);
return result.changes > 0;
}7.4: Router
// src/router.ts
import {
getAllTasks,
getTaskById,
createTask,
updateTask,
deleteTask,
} from "./handlers/tasks";
export async function handleRequest(req: Request): Promise<Response> {
const url = new URL(req.url);
const path = url.pathname;
const method = req.method;
// CORS headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
if (method === "OPTIONS") {
return new Response(null, { status: 204, headers });
}
// GET /api/tasks
if (path === "/api/tasks" && method === "GET") {
const tasks = getAllTasks();
return Response.json({ data: tasks }, { headers });
}
// GET /api/tasks/:id
const taskMatch = path.match(/^\/api\/tasks\/(\d+)$/);
if (taskMatch && method === "GET") {
const task = getTaskById(Number(taskMatch[1]));
if (!task) {
return Response.json(
{ error: "Task not found" },
{ status: 404, headers }
);
}
return Response.json({ data: task }, { headers });
}
// POST /api/tasks
if (path === "/api/tasks" && method === "POST") {
const body = await req.json();
if (!body.title || typeof body.title !== "string") {
return Response.json(
{ error: "Title is required" },
{ status: 400, headers }
);
}
const task = createTask(body);
return Response.json({ data: task }, { status: 201, headers });
}
// PUT /api/tasks/:id
if (taskMatch && method === "PUT") {
const body = await req.json();
const task = updateTask(Number(taskMatch[1]), body);
if (!task) {
return Response.json(
{ error: "Task not found" },
{ status: 404, headers }
);
}
return Response.json({ data: task }, { headers });
}
// DELETE /api/tasks/:id
if (taskMatch && method === "DELETE") {
const deleted = deleteTask(Number(taskMatch[1]));
if (!deleted) {
return Response.json(
{ error: "Task not found" },
{ status: 404, headers }
);
}
return Response.json({ message: "Task deleted" }, { headers });
}
return Response.json({ error: "Not Found" }, { status: 404, headers });
}7.5: Entry Point
// src/index.ts
import { handleRequest } from "./router";
const server = Bun.serve({
port: Bun.env.PORT ? Number(Bun.env.PORT) : 3000,
fetch: handleRequest,
});
console.log(`Tasks API running at http://localhost:${server.port}`);Running the API
bun run src/index.tsTest with curl:
# Create a task
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Learn Bun", "description": "Complete the Bun tutorial"}'
# List all tasks
curl http://localhost:3000/api/tasks
# Update a task
curl -X PUT http://localhost:3000/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# Delete a task
curl -X DELETE http://localhost:3000/api/tasks/1Step 8: The Bun Test Runner
Bun includes a Jest-compatible test runner with built-in assertions.
Writing Tests
// tests/tasks.test.ts
import { describe, it, expect, beforeAll, afterAll } from "bun:test";
const BASE_URL = "http://localhost:3000";
let server: any;
let createdTaskId: number;
beforeAll(async () => {
// Start the server for testing
const module = await import("../src/index");
});
describe("Tasks API", () => {
it("should create a task", async () => {
const res = await fetch(`${BASE_URL}/api/tasks`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "Test Task",
description: "A test task",
}),
});
expect(res.status).toBe(201);
const json = await res.json();
expect(json.data.title).toBe("Test Task");
createdTaskId = json.data.id;
});
it("should list all tasks", async () => {
const res = await fetch(`${BASE_URL}/api/tasks`);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.data).toBeInstanceOf(Array);
expect(json.data.length).toBeGreaterThan(0);
});
it("should get a task by ID", async () => {
const res = await fetch(`${BASE_URL}/api/tasks/${createdTaskId}`);
expect(res.status).toBe(200);
const json = await res.json();
expect(json.data.id).toBe(createdTaskId);
});
it("should update a task", async () => {
const res = await fetch(`${BASE_URL}/api/tasks/${createdTaskId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ completed: true }),
});
expect(res.status).toBe(200);
const json = await res.json();
expect(json.data.completed).toBe(1);
});
it("should return 400 for missing title", async () => {
const res = await fetch(`${BASE_URL}/api/tasks`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
expect(res.status).toBe(400);
});
it("should return 404 for non-existent task", async () => {
const res = await fetch(`${BASE_URL}/api/tasks/99999`);
expect(res.status).toBe(404);
});
});Running Tests
# Run all tests
bun test
# Run a specific test file
bun test tests/tasks.test.ts
# Watch mode
bun test --watch
# With coverage
bun test --coverageBun's test runner is significantly faster than Jest — typical test suites run in milliseconds rather than seconds.
Step 9: The Bun Bundler
Bun includes a production-grade bundler that replaces webpack, esbuild, and Rollup.
Basic Bundling
# Bundle a file for the browser
bun build ./src/client.ts --outdir ./dist
# Bundle for Node.js compatibility
bun build ./src/index.ts --target node --outdir ./dist
# Bundle for Bun
bun build ./src/index.ts --target bun --outdir ./distProgrammatic API
// build.ts
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
target: "bun",
minify: true,
splitting: true,
sourcemap: "external",
});
if (!result.success) {
console.error("Build failed:");
for (const log of result.logs) {
console.error(log);
}
process.exit(1);
}
console.log(`Build complete: ${result.outputs.length} files`);Creating a Standalone Executable
One of Bun's most powerful features is compiling your application into a single executable:
bun build ./src/index.ts --compile --outfile tasks-apiThis produces a standalone binary that includes the Bun runtime. You can distribute it to any machine without requiring Bun or Node.js to be installed:
./tasks-api
# Tasks API running at http://localhost:3000Step 10: WebSocket Server
Bun has first-class WebSocket support built into the HTTP server:
// ws-server.ts
Bun.serve({
port: 3001,
fetch(req, server) {
// Upgrade HTTP request to WebSocket
if (server.upgrade(req)) {
return; // Upgrade successful
}
return new Response("WebSocket server", { status: 200 });
},
websocket: {
open(ws) {
console.log("Client connected");
ws.subscribe("chat");
},
message(ws, message) {
// Broadcast to all subscribers
ws.publish("chat", `User: ${message}`);
},
close(ws) {
console.log("Client disconnected");
ws.unsubscribe("chat");
},
},
});
console.log("WebSocket server running on ws://localhost:3001");Bun's WebSocket implementation handles over 1 million messages per second — making it ideal for real-time applications.
Step 11: Script Running and package.json
Bun can run package.json scripts much faster than npm:
{
"name": "bun-tasks-api",
"scripts": {
"dev": "bun --watch run src/index.ts",
"start": "bun run src/index.ts",
"build": "bun build src/index.ts --compile --outfile dist/tasks-api",
"test": "bun test",
"test:watch": "bun test --watch",
"lint": "bunx @biomejs/biome check ./src",
"db:seed": "bun run scripts/seed.ts"
}
}Note the --watch flag — Bun has built-in file watching with hot reload, replacing tools like nodemon.
Using bunx
bunx is Bun's equivalent of npx — it runs packages without installing them globally:
bunx create-next-app my-app
bunx prisma migrate dev
bunx @biomejs/biome check ./srcStep 12: Deploying to Production
Option A: Docker
Create a minimal Dockerfile:
FROM oven/bun:2 AS base
WORKDIR /app
# Install dependencies
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
# Copy source
COPY src/ src/
# Run
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]Build and run:
docker build -t bun-tasks-api .
docker run -p 3000:3000 bun-tasks-apiOption B: Standalone Binary
Compile and deploy a single binary:
# Build on your machine
bun build src/index.ts --compile --outfile tasks-api
# Copy to server and run
scp tasks-api user@server:/opt/app/
ssh user@server "chmod +x /opt/app/tasks-api && /opt/app/tasks-api"Option C: Fly.io
# fly.toml
app = "bun-tasks-api"
[build]
dockerfile = "Dockerfile"
[http_service]
internal_port = 3000
force_https = true
[[vm]]
size = "shared-cpu-1x"
memory = "256mb"fly launch
fly deployPerformance Benchmarks
Here is a comparison of Bun versus Node.js for common operations (lower is better):
| Operation | Node.js 22 | Bun 2.0 | Speedup |
|---|---|---|---|
| Package install (Next.js) | 28s | 1.8s | 15x |
| TypeScript execution | 320ms | 45ms | 7x |
| HTTP server (req/s) | 68,000 | 260,000 | 3.8x |
| File read (1GB) | 1.2s | 0.4s | 3x |
| Test suite (100 tests) | 4.5s | 0.8s | 5.6x |
| Bundle (React app) | 1.1s | 0.3s | 3.7x |
These numbers come from typical real-world workloads. Your results may vary depending on hardware and project complexity.
Troubleshooting
Common Issues
Node.js packages that use native addons:
Some npm packages with C++ addons may not work with Bun. Check the Bun compatibility tracker at bun.sh/ecosystem for known issues.
Memory usage: Bun uses JavaScriptCore instead of V8. Memory behavior differs — some workloads use less memory, others more. Monitor with:
bun run --smol src/index.ts # Reduces memory usage at the cost of some performanceMissing Node.js APIs: Bun implements most Node.js APIs, but some edge cases may differ. If you hit an incompatibility, check the Bun documentation for workarounds.
Next Steps
Now that you have mastered the fundamentals of Bun 2.0, explore these areas:
- Hono framework — Use Hono on Bun for a more structured API with middleware
- Elysia framework — Type-safe APIs with end-to-end type inference on Bun
- Bun Shell — Script system tasks using Bun's built-in shell API
- SQLite with Drizzle ORM — Combine Bun's native SQLite with Drizzle for type-safe queries
- WebSocket applications — Build real-time chat or notification systems
Conclusion
Bun 2.0 is a paradigm shift for JavaScript development. By consolidating the runtime, package manager, bundler, and test runner into a single binary, it eliminates the toolchain complexity that has plagued the JavaScript ecosystem for years.
In this guide, you built a complete REST API with SQLite persistence, learned to use the Bun test runner, bundled code for production, and explored deployment options. The speed gains alone make Bun worth considering for new projects, and its Node.js compatibility means you can adopt it incrementally.
The JavaScript runtime landscape has evolved. Bun 2.0 is leading the charge.
Discuss Your Project with Us
We're here to help with your web development needs. Schedule a call to discuss your project and how we can assist you.
Let's find the best solutions for your needs.
Related Articles

Biome: Replace ESLint and Prettier with One Ultra-Fast Tool
Learn how to migrate from ESLint + Prettier to Biome, the ultra-fast Rust-powered linter and formatter. Configuration, custom rules, CI/CD integration, and VS Code setup — all in one tool.

End-to-End Testing with Playwright and Next.js: From Zero to CI Pipeline
Learn how to set up Playwright for end-to-end testing in a Next.js application. This hands-on tutorial covers setup, Page Object Model, visual regression, accessibility testing, and CI/CD integration with GitHub Actions.

Vitest and React Testing Library with Next.js 15: The Complete Unit Testing Guide for 2026
Learn how to set up and master Vitest with React Testing Library in a Next.js 15 project. This tutorial covers component testing, hooks, Server Components, API routes, and CI/CD integration.