Skip to main content
Technology & EngineeringBun352 lines

Bun Node.js Migration

Migrating from Node.js to Bun: compatibility checklist, node:* module imports, native addon handling, environment variable differences, Docker setup, CI/CD pipeline changes, and common migration pitfalls.

Quick Summary32 lines
You are an expert in migrating JavaScript and TypeScript projects from Node.js to Bun, covering compatibility assessment, incremental migration strategies, and production deployment considerations.

## Key Points

- [ ] Node.js built-in modules used (list them, check Bun support)
- [ ] Native addons (.node files, node-gyp) -- these are the most common blocker
- [ ] Package.json scripts -- check for node-specific flags
- [ ] TypeScript configuration -- Bun has its own tsconfig defaults
- [ ] Test framework -- can switch to bun test or keep existing
- [ ] Environment variable loading (.env files) -- Bun loads them automatically
- [ ] Process management (PM2, cluster) -- needs Bun-specific approach
- [ ] Debugging setup (--inspect) -- Bun uses --inspect with WebKit devtools
- [ ] CI/CD pipeline -- Docker images, install commands
- [ ] Production monitoring (APM tools) -- check Bun compatibility
1. Find a pure JS/WASM alternative
2. Use Bun's FFI to call the native library directly

## Quick Example

```bash
curl -fsSL https://bun.sh/install | bash
```

```bash
# Try running your existing test suite with Bun
bun test

# Or if using a custom test command
bun run test
```
skilldb get bun-skills/Bun Node.js MigrationFull skill: 352 lines
Paste into your CLAUDE.md or agent config

Bun Node.js Migration — Switching from Node to Bun

You are an expert in migrating JavaScript and TypeScript projects from Node.js to Bun, covering compatibility assessment, incremental migration strategies, and production deployment considerations.

Migration Overview

Migrating from Node.js to Bun is not an all-or-nothing switch. Bun implements the vast majority of the Node.js API surface, so most projects can run on Bun with minimal changes. The migration process involves: assessing compatibility, switching the package manager, updating scripts and CI, testing thoroughly, and deploying.

Start by running your existing test suite under Bun. If tests pass, you are in good shape. If they fail, the failures will guide you to the specific incompatibilities you need to address.

Compatibility Checklist

Run through this checklist before migrating:

- [ ] Node.js built-in modules used (list them, check Bun support)
- [ ] Native addons (.node files, node-gyp) -- these are the most common blocker
- [ ] Package.json scripts -- check for node-specific flags
- [ ] TypeScript configuration -- Bun has its own tsconfig defaults
- [ ] Test framework -- can switch to bun test or keep existing
- [ ] Environment variable loading (.env files) -- Bun loads them automatically
- [ ] Process management (PM2, cluster) -- needs Bun-specific approach
- [ ] Debugging setup (--inspect) -- Bun uses --inspect with WebKit devtools
- [ ] CI/CD pipeline -- Docker images, install commands
- [ ] Production monitoring (APM tools) -- check Bun compatibility

Step-by-Step Migration

Step 1: Install Bun

curl -fsSL https://bun.sh/install | bash

Step 2: Switch Package Manager

# Remove old lockfile and node_modules
rm -rf node_modules package-lock.json yarn.lock pnpm-lock.yaml

# Install with Bun
bun install

# Verify the lockfile was created
ls bun.lockb

Step 3: Run Tests

# Try running your existing test suite with Bun
bun test

# Or if using a custom test command
bun run test

Step 4: Update package.json Scripts

{
  "scripts": {
    "dev": "bun --watch run src/server.ts",
    "build": "bun build src/index.ts --outdir dist",
    "start": "bun run dist/index.js",
    "test": "bun test",
    "lint": "bun run eslint .",
    "typecheck": "bun run tsc --noEmit"
  }
}

Note: bun run eslint . still uses the eslint binary, but launches it via Bun. For tools that must run on Node.js, use bunx --bun=false node script.js or keep Node.js installed alongside Bun.

Step 5: Update TypeScript Config

// tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["bun-types"],
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./dist"
  }
}
bun add -d bun-types

node:* Imports

Bun supports the node: prefix for built-in modules, and it is the recommended way to import them:

// Preferred -- explicit node: prefix
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { createHash } from "node:crypto";

// Also works -- bare specifier (for backward compat)
import { readFile } from "fs/promises";

Module Compatibility Matrix

ModuleStatusNotes
node:fsFullAll sync and async APIs
node:pathFull
node:osFull
node:cryptoFullIncludes Web Crypto API
node:httpFullBun.serve() is faster
node:httpsFull
node:netFull
node:tlsFull
node:streamFull
node:eventsFull
node:bufferFull
node:urlFull
node:utilFull
node:child_processFullBun.spawn() is faster
node:worker_threadsFull
node:zlibFull
node:assertFull
node:dnsFull
node:readlineFull
node:clusterPartialBasic functionality works
node:vmPartialSome sandboxing features differ
node:v8StubNot applicable (JavaScriptCore)
node:inspectorMinimalDifferent debugging protocol
node:diagnostics_channelPartialBasic support

Native Addons

Native addons are the biggest migration risk. Bun implements N-API (Node-API) which covers most modern addons, but older addons using the legacy V8 C++ API will not work.

# Check which packages use native addons
find node_modules -name "*.node" -o -name "binding.gyp" | head -20

Common packages and their Bun status:

PackageStatusAlternative
better-sqlite3Works (N-API)Use bun:sqlite instead
bcryptWorks (N-API)Use Bun.password.hash()
sharpWorks
canvasWorks
argon2Works (N-API)Use Bun.password.hash()
node-sassDoes not workUse sass (Dart Sass)
cpu-featuresDoes not workUsually not critical

If a critical native addon does not work, options include:

  1. Find a pure JS/WASM alternative
  2. Use Bun's FFI to call the native library directly
  3. Keep that specific service on Node.js

Environment Variables

Bun automatically loads .env files without dotenv:

# Loading order (later files override earlier ones):
# .env
# .env.local
# .env.development (when NODE_ENV=development)
# .env.production (when NODE_ENV=production)
// Remove dotenv from your project
// Before (Node.js):
import "dotenv/config"; // DELETE THIS

// After (Bun):
// Just access env vars directly -- Bun loads .env automatically
const port = process.env.PORT ?? "3000";
// or use Bun.env for typed access
const port = Bun.env.PORT ?? "3000";
bun remove dotenv  # no longer needed

Docker Setup

# Production Dockerfile
FROM oven/bun:1-alpine AS base
WORKDIR /app

# Install dependencies
FROM base AS deps
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production

# Build stage (if you have a build step)
FROM base AS build
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build

# Production image
FROM base AS runtime
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./

# Run as non-root user
USER bun
EXPOSE 3000

CMD ["bun", "run", "dist/index.js"]
# docker-compose.yml
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgres://db:5432/app
    volumes:
      - ./data:/app/data

CI/CD Changes

GitHub Actions

name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest

      - run: bun install --frozen-lockfile
      - run: bun run typecheck
      - run: bun run lint
      - run: bun test --coverage

GitLab CI

test:
  image: oven/bun:1
  script:
    - bun install --frozen-lockfile
    - bun test

Common Pitfalls

1. Global this Differs

// In Node.js, `this` at the top level of a CJS module is `exports`
// In Bun ESM, `this` is `undefined`
// Fix: do not rely on top-level `this`

2. Error Stack Traces

// Bun's stack traces look different from Node.js
// Line numbers may differ slightly due to TypeScript handling
// Do not parse stack traces in production code

3. Process Lifecycle

// Node.js keeps running with active event listeners
// Bun also does this, but some edge cases differ

// Explicitly keep the process alive if needed:
setInterval(() => {}, 1 << 30); // keep alive

// Or handle graceful shutdown:
process.on("SIGTERM", () => {
  console.log("Shutting down...");
  server.stop();
  process.exit(0);
});

4. Timing Differences

// setTimeout/setInterval resolution may differ slightly
// Do not write tests that depend on exact timing
// Use Bun's mock timers in tests instead

5. Module Resolution Order

// Bun resolves modules slightly differently in edge cases:
// - Bun prefers "module" over "main" in package.json
// - Bun supports "exports" field fully
// - Bun reads "bun" field in package.json for Bun-specific entry points

// If a package resolves incorrectly, check its package.json "exports" map

Incremental Migration Strategy

For large projects, migrate incrementally:

  1. Week 1: Switch to Bun package manager only (bun install). Keep Node.js as the runtime. This is risk-free.
  2. Week 2: Run the test suite with bun test. Fix any failures.
  3. Week 3: Switch development servers to Bun (bun --watch run dev). Developers get faster iteration.
  4. Week 4: Deploy a non-critical service on Bun in production. Monitor for issues.
  5. Week 5+: Migrate remaining services one at a time.

Anti-Patterns

  • Big-bang migration of all services at once: Migrate one service at a time. Run both Node.js and Bun in production during the transition.
  • Removing Node.js before verifying all dependencies work: Keep Node.js installed as a fallback until you have confirmed full compatibility in production.
  • Not running the full test suite before deploying: Bun compatibility is good but not perfect. Your tests are the source of truth.
  • Assuming identical performance characteristics: While Bun is generally faster, some workloads may perform differently. Benchmark your specific use case.
  • Ignoring native addon warnings during bun install: Warnings about native addons are signals of potential runtime failures. Investigate each one.
  • Using node command in scripts after migrating: Update all scripts, Dockerfiles, and CI configs to use bun instead of node. Mixed runtimes cause confusion.

Install this skill directly: skilldb add bun-skills

Get CLI access →