Bun 2.0: Finally Replaced Node.js Without Breaking Everything

I tried switching to Bun 1.0 when it launched. Three native modules failed, my Prisma setup broke, and I went back to Node.js in two hours. Bun 2.0 is different. I've been running it in production for 4 months across two API servers and a cron job runner. Here's what actually changed, what still breaks, and the migration path that worked.

What Changed in Bun 2.0

The biggest changes from 1.0 to 2.0 that actually affected my projects:

Migration Checklist from Node.js

Here's the exact migration path I followed for an Express API with 40K lines of TypeScript:

# 1. Install Bun
curl -fsSL https://bun.sh/install | bash

# 2. Replace node with bun in scripts
# Before:
# "start": "node dist/index.js"
# "dev": "ts-node-dev src/index.ts"
# After:
# "start": "bun dist/index.js"
# "dev": "bun --watch src/index.ts"

# 3. Switch package manager
rm -rf node_modules package-lock.json
bun install  # Creates bun.lockb

# 4. Replace test runner
# Before: jest --coverage
# After: bun test --coverage

# 5. Build TypeScript (no more tsc step)
# Before: tsc && node dist/index.js
# After: bun src/index.ts  # Runs TS directly

The package.json scripts before and after:

{
  "scripts": {
    "dev": "bun --watch src/index.ts",
    "build": "bun build src/index.ts --outdir dist --target bun",
    "start": "bun dist/index.js",
    "test": "bun test",
    "lint": "bunx biome check src/"
  }
}

Built-in Tools That Replace 10 Dependencies

This is where Bun saves the most time. Here's what I removed from my dependencies:

# Dependencies I removed:
# - typescript (bun runs TS natively)
# - ts-node / tsx (bun runs TS natively)
# - jest / vitest (bun test replaces both)
# - esbuild / webpack (bun build replaces both)
# - nodemon (bun --watch replaces it)
# - dotenv (bun reads .env automatically)
# - cross-env (bun handles env vars cross-platform)

# What I kept:
# - prisma (ORM)
# - express → migrated to hono (faster on bun)
# - zod (validation)

Bun Test Examples

// tests/api.test.ts
import { describe, test, expect, beforeAll } from "bun:test"
import { app } from "../src/app"

describe("User API", () => {
  test("GET /users returns list", async () => {
    const res = await app.request("/api/users")
    expect(res.status).toBe(200)
    const data = await res.json()
    expect(Array.isArray(data)).toBe(true)
  })

  test("POST /users creates user", async () => {
    const res = await app.request("/api/users", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name: "Test", email: "test@example.com" }),
    })
    expect(res.status).toBe(201)
  })
})

// Run with: bun test
// Output is 10-15x faster than Jest for the same tests

Bun Build for APIs

# Bundle API for deployment
bun build src/index.ts \
  --outdir dist \
  --target bun \
  --minify \
  --sourcemap

# Output: single file in dist/index.js
# Size comparison:
# - Node.js with tsc: 40K lines across 200 files
# - Bun bundled: 2.3MB single file
# - Startup: 12ms vs Node.js 340ms

Problem

After migrating to Bun 2.0, my password hashing broke. The bcrypt npm package (which uses N-API bindings) caused segfaults intermittently. Worked fine on Node.js 22.

What I Tried

Tried rebuilding the native module with bun install --force. Tried using bcryptjs (pure JS version) — that worked but was 3x slower. Tried argon2 which also had N-API issues.

Actual Fix

Bun 2.0 has built-in password hashing via Bun.password. No native module needed:

// Built-in password hashing — no npm package needed
const password = "user-password-123"

// Hash (uses bcrypt or argon2 internally)
const hash = await Bun.password.hash(password, {
  algorithm: "argon2id", // or "bcrypt"
  memoryCost: 65536,
  timeCost: 3,
})

// Verify
const valid = await Bun.password.verify(password, hash)
console.log(valid) // true

// This replaced both bcrypt AND argon2 npm packages

Performance Benchmarks That Actually Matter

I benchmarked my actual API (not a hello-world benchmark) on the same hardware. The API does JSON parsing, PostgreSQL queries via Prisma, and JWT verification on every request.

# Benchmark: wrk -t4 -c100 -d30s http://localhost:3000/api/users
#
# Node.js 22 + Express:
#   Requests/sec: 3,420
#   Latency avg: 14.2ms
#   P99: 89ms
#
# Bun 2.0 + Hono:
#   Requests/sec: 11,850
#   Latency avg: 4.1ms
#   P99: 32ms
#
# Startup time:
#   Node.js cold start: 340ms
#   Bun cold start: 12ms
#
# Test suite (140 tests):
#   Jest: 8.2s
#   Bun test: 0.6s

The 3.5x throughput improvement isn't just from Bun being faster at JavaScript execution. It's the combination of faster startup, better HTTP parsing, and Hono's lightweight routing (vs Express's heavier middleware stack).

Problem

My file upload handler used stream.pipeline from Node.js to pipe request bodies to disk. On Node.js it worked perfectly. On Bun 2.0, large uploads (>50MB) would silently truncate — the file on disk was smaller than the actual upload.

What I Tried

Tried adding explicit error handlers to the pipeline. Tried using fs.createWriteStream with manual .write() calls. Both had the same issue.

Actual Fix

Bun has its own file I/O API that handles streaming correctly. I replaced the Node.js stream approach with Bun's native file writing:

// Bun-native file writing — works correctly for large files
import { write } from "bun"

async function handleUpload(request: Request) {
  const formData = await request.formData()
  const file = formData.get("file") as File

  // Bun's write handles large files correctly
  const bytes = await file.arrayBuffer()
  await write(`uploads/${file.name}`, bytes)

  // Or use Bun.file() for even better performance
  await Bun.write(`uploads/${file.name}`, file)

  return Response.json({ uploaded: file.name, size: file.size })
}

Native Module Compatibility Gotchas

Not everything is smooth. Here's what I still need workarounds for:

// Built-in SQLite — no npm package needed
const db = Bun.sqlite(":memory:")
// Or: Bun.sqlite("mydata.db")

db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
db.run("INSERT INTO users (name) VALUES (?)", ["Alice"])

const users = db.query("SELECT * FROM users").all()
console.log(users) // [{ id: 1, name: "Alice" }]

// For Prisma users, just use prisma as normal
// Bun 2.0 handles the native engine correctly now

What I Learned

Wrapping Up

Bun 2.0 is the first version I'd recommend for production without caveats. The Node.js compatibility is good enough that most projects migrate with minimal changes. The built-in tools save real time — my CI pipeline went from 4 minutes to 45 seconds just by switching from Jest to bun test. If you're starting a new TypeScript project in 2026, Bun should be your default runtime.

Related Articles