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.
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 linesBun 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
| Module | Status | Notes |
|---|---|---|
| node:fs | Full | All sync and async APIs |
| node:path | Full | |
| node:os | Full | |
| node:crypto | Full | Includes Web Crypto API |
| node:http | Full | Bun.serve() is faster |
| node:https | Full | |
| node:net | Full | |
| node:tls | Full | |
| node:stream | Full | |
| node:events | Full | |
| node:buffer | Full | |
| node:url | Full | |
| node:util | Full | |
| node:child_process | Full | Bun.spawn() is faster |
| node:worker_threads | Full | |
| node:zlib | Full | |
| node:assert | Full | |
| node:dns | Full | |
| node:readline | Full | |
| node:cluster | Partial | Basic functionality works |
| node:vm | Partial | Some sandboxing features differ |
| node:v8 | Stub | Not applicable (JavaScriptCore) |
| node:inspector | Minimal | Different debugging protocol |
| node:diagnostics_channel | Partial | Basic 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:
| Package | Status | Alternative |
|---|---|---|
| better-sqlite3 | Works (N-API) | Use bun:sqlite instead |
| bcrypt | Works (N-API) | Use Bun.password.hash() |
| sharp | Works | |
| canvas | Works | |
| argon2 | Works (N-API) | Use Bun.password.hash() |
| node-sass | Does not work | Use sass (Dart Sass) |
| cpu-features | Does not work | Usually not critical |
If a critical native addon does not work, options include:
- Find a pure JS/WASM alternative
- Use Bun's FFI to call the native library directly
- 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:
- Week 1: Switch to Bun package manager only (
bun install). Keep Node.js as the runtime. This is risk-free. - Week 2: Run the test suite with
bun test. Fix any failures. - Week 3: Switch development servers to Bun (
bun --watch run dev). Developers get faster iteration. - Week 4: Deploy a non-critical service on Bun in production. Monitor for issues.
- 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
nodecommand in scripts after migrating: Update all scripts, Dockerfiles, and CI configs to usebuninstead ofnode. Mixed runtimes cause confusion.
Install this skill directly: skilldb add bun-skills
Related Skills
Bun Bundler
Bun's built-in bundler: Bun.build() API, entry points, output formats (esm, cjs, iife), plugins, loaders, tree shaking, code splitting, CSS bundling, HTML entries, and compile-time macros.
Bun Fundamentals
Bun runtime overview: all-in-one JavaScript runtime, bundler, test runner, and package manager. Installation, project initialization, Node.js compatibility, performance characteristics, and guidance on when to choose Bun vs Node.
Bun HTTP Server
Building HTTP servers with Bun: Bun.serve() API, routing patterns, WebSocket support, streaming responses, static file serving, TLS configuration, hot reloading, and integration with frameworks like Hono and Elysia.
Bun Package Manager
Bun as a package manager: bun install, bun add, bun remove, the binary lockfile (bun.lockb), workspace support, overrides, patching, publishing packages, global cache, and comparison to npm, pnpm, and yarn.
Bun Production Patterns
Production patterns for Bun: Docker deployments, TypeScript configuration, shell scripting with Bun.$, monorepo setup, database access patterns, and deployment to Fly.io and Railway.
Bun Runtime APIs
Bun-native runtime APIs including Bun.serve(), Bun.file(), Bun.write(), Bun.spawn(), Bun.sleep(), Bun.env, FFI for calling native libraries, built-in SQLite, S3 client, glob, and semver utilities.