T
he switch from Node.js to Bun is not a migration. There's no "migration guide" to follow, no compatibility matrix to consult for most projects. You change the runtime, update a few scripts, and the project runs faster. That's mostly how it goes.We've moved several client projects off Node.js onto Bun over the past eighteen months. Not because Bun is fashionable because npm install taking 45 seconds per CI run was adding up, and because running TypeScript files directly without a compilation step turned out to matter more for developer experience than we expected.
This is what happened: the real numbers, the real friction, and the cases where Bun isn't the right call yet.
What Bun Actually Is
Bun is a JavaScript runtime, package manager, bundler, and test runner built on JavaScriptCore instead of V8, written in Zig. It's not a Node.js wrapper or a compatibility layer. It's a separate implementation that happens to support the Node.js API surface.
That last part matters. Bun runs most Node.js code without changes because it implements node:fs, node:http, node:crypto, and the rest of the standard library. It also supports npm packages natively your existing package.json and node_modules work as-is.
What it adds: TypeScript execution without compilation, faster I/O, a built-in test runner, and a package manager that's genuinely faster than npm or yarn.
What it doesn't add: complexity. The binary is a single file. There's no ecosystem of plugins to configure.
The Speed Numbers That Actually Matter
Package installation is where Bun's difference is most visible in CI.
A project with 180 dependencies: npm install took 52 seconds cold, 18 seconds with cache. bun install took 8 seconds cold, 1.2 seconds with cache. On a team running CI 20 times a day, that's 16 minutes of CI time saved per day on install alone.
TypeScript execution is also faster, but the gap depends on what your code does. CPU-bound work in tight loops Bun is meaningfully faster than Node.js. I/O-bound work where you're mostly waiting on network or disk the difference is smaller, often within noise.
Test running: Bun's built-in test runner uses the same describe/it/expect API as Jest. For a project with 400 unit tests, we saw runs drop from 8 seconds with Jest to 1.1 seconds with Bun's runner. That's not a 10% improvement. It's a feedback loop change.
The number that surprised us most: developer workflow. Running bun run src/scripts/migrate.ts directly, without a ts-node or tsx invocation, without waiting for compilation, changes how often people actually run scripts. Small thing. Real effect.
The Migration, Step by Step
For a standard Node.js + TypeScript project:
Untitled1# Install Bun2curl -fsSL https://bun.sh/install | bash34# In your project5bun install # reads existing package.json, installs to node_modules
That's it for dependencies. Your existing node_modules structure is compatible.
For scripts in package.json:
Untitled1{2 "scripts": {3 "dev": "bun run --watch src/index.ts",4 "build": "bun build src/index.ts --outdir dist --target node",5 "test": "bun test"6 }7}
bun run --watch replaces ts-node-dev or nodemon. It reloads on file changes and runs TypeScript directly.
For CI, replace npm ci with bun install --frozen-lockfile. The lockfile is bun.lockb a binary format that's not human-readable but is faster to parse than package-lock.json. Commit it.
One thing to update: if you have "type": "module" in package.json and you're using NodeNext module resolution in tsconfig, local imports need the .js extension. Bun respects TypeScript's module resolution rules.
Untitled1// This:2import { db } from "./database.js";34// Not this:5import { db } from "./database";
The .js extension looks wrong when the file is .ts. It's correct. TypeScript resolves .ts at compile time when you reference .js. Bun does the same thing at runtime. This trips people up the first time; after that, it's fine.
The Part Most Teams Get Wrong
They test Bun on a greenfield project and then assume migrating an existing project is the same experience.
It usually is. The cases where it isn't:
Native addons. If you're using packages that have native bindings compiled C++ code in node_modules those bindings are compiled against the Node.js ABI, not Bun's. Packages like sharp, bcrypt (native version), canvas, and some database drivers with native mode will fail or fall back silently. Check your dependencies before committing to the switch.
Node.js version-specific APIs. Bun tracks Node.js compatibility, but it's not always at the latest version's surface area. If you're using something from Node.js 22 that was very recently added, check Bun's compatibility tracker before assuming it works.
Worker threads. Bun supports worker_threads, but the behavior in some edge cases differs from Node.js. If you're doing significant work with workers, test thoroughly.
For most API servers, CLI tools, and backend services that don't touch native addons: the migration is clean.
Real-World Example: Vercel's Internal Tooling Switch
Vercel published benchmarks when they moved internal tooling to Bun. The numbers that stood out: test suite times dropped from minutes to seconds, and local development start times were cut in half for TypeScript projects that previously needed compilation before running.
The pattern is consistent across teams we've worked with. A Series A startup running a Node.js API with TypeScript cut their CI time from 6 minutes to 3.5 minutes after switching to Bun for install and test running without changing any application code. The remaining 3.5 minutes was actual work: building, deploying, running integration tests against a real database. The tooling overhead was cut nearly in half.
What didn't change: any of the application behavior, the API contracts, the database layer. Bun isn't a framework decision. It's an infrastructure decision at the development layer.
FAQ
Q: Should we use Bun's bundler instead of webpack or esbuild? A: For simple projects, yes. Bun's bundler handles TypeScript, JSX, tree-shaking, and code splitting. For projects with complex webpack configs custom loaders, specific chunk strategies, module federation migrate the bundler separately and later. Don't block the runtime switch on the bundler switch.
Q: Does Bun work with Docker?
A: Yes. Official Bun Docker images exist at oven/bun. The pattern is: use bun install --frozen-lockfile in the build stage, copy the output, run with bun run. The images are smaller than their Node.js equivalents because the Bun binary is self-contained.
Q: What about ts-node compatibility?
A: You don't need it. bun run file.ts replaces ts-node file.ts and npx ts-node file.ts. For tsconfig-paths support (path aliases), Bun reads your tsconfig directly and resolves aliases natively.
Q: Does Bun support all Jest matchers?
A: The built-in test runner supports expect, describe, it, beforeEach, afterEach, beforeAll, afterAll, and most matchers. It doesn't support every Jest plugin or custom serializer. If you're on Jest with significant custom configuration, test the runner separately before switching. For projects running standard Jest without plugins, the switch is usually clean.
Q: Can we use Bun in production or just development?
A: Both. Bun runs as a production runtime. For a typical Node.js API server, the production deployment is bun run dist/index.js or bun run src/index.ts directly. Several companies run Bun in production at scale. The decision is: are your dependencies compatible, and are you comfortable with a runtime that's newer than Node.js LTS? For most teams at Seed to Series B, the answer is yes.
The switch is worth making earlier than most teams make it. CI speed and developer feedback loops are infrastructure too just the kind that's easy to ignore because it doesn't affect users directly.
If you haven't set up the rest of your TypeScript toolchain yet, the infrastructure stack guide covers the full picture. The Bun switch is one piece of it.
Internal Reference Logs:
External Documentation:
- [Bun Official Documentation] Runtime API reference and compatibility tracker.
- [Bun Node.js Compatibility] Which Node.js APIs are supported and at what level.