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

AI Bot
By AI Bot ·

Loading the Text to Speech Audio Player...

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:

FeatureTraditional StackBun
RuntimeNode.jsBun
Package Managernpm / yarn / pnpmbun install
Bundlerwebpack / esbuild / Rollupbun build
Test RunnerJest / Vitestbun test
TypeScripttsc + ts-nodeNative support
.env loadingdotenv packageBuilt-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 | bash

On macOS, you can also use Homebrew:

brew install oven-sh/bun/bun

Verify the installation:

bun --version
# 2.x.x

Bun installs to ~/.bun/bin/bun and automatically adds itself to your PATH.

Updating Bun

Keep Bun up to date with:

bun upgrade

Step 2: Your First Bun Project

Create a new project using bun init:

mkdir bun-demo && cd bun-demo
bun init

This 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 zod

Speed 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.lockb

Workspaces

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 start

Step 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 type

Bun.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); // true

5.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.ts

The 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 tests

7.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.ts

Test 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/1

Step 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 --coverage

Bun'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 ./dist

Programmatic 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-api

This 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:3000

Step 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 ./src

Step 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-api

Option 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 deploy

Performance Benchmarks

Here is a comparison of Bun versus Node.js for common operations (lower is better):

OperationNode.js 22Bun 2.0Speedup
Package install (Next.js)28s1.8s15x
TypeScript execution320ms45ms7x
HTTP server (req/s)68,000260,0003.8x
File read (1GB)1.2s0.4s3x
Test suite (100 tests)4.5s0.8s5.6x
Bundle (React app)1.1s0.3s3.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 performance

Missing 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.


Want to read more tutorials? Check out our latest tutorial on Exploring the New Responses API: A Comprehensive Guide.

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