Changing the package manager was the easy part of moving this blog to Bun. The useful work happened after that: replacing file operations, updating CI, and finding out which Node assumptions still held.
Part 1 covers why I made the switch. This is the migration itself.
Start with the Smallest Change
I started with the scripts in package.json:
// Before (package.json scripts)
"scripts": {
"start": "ts-node src/index.ts",
"build": "tsc",
"dev": "nodemon --watch src --exec ts-node src/index.ts",
}
// After
"scripts": {
"start": "bun src/index.ts",
"build": "bun build ./src/index.ts --outdir ./dist --target=bun",
"dev": "bun --watch src/index.ts",
}
Bun runs TypeScript directly, so I could remove the separate compile step. Its bundler also replaced the build configuration I had accumulated around TypeScript.
Then I moved CI:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.2.11
- run: bun install
- run: bun run build
- run: bun run typecheck
That replaced a 4,500-line package-lock.json with bun.lock.
The File APIs Were the Real Migration
The biggest payoff came from converting Node’s fs operations to Bun’s native File API.
I used Claude Code to find the file-system calls and review them one at a time:
- Scan for patterns like
fs.readFileandfs.pathExistsacross the codebase - Group the results by how each file was used
- Compare the Node and Bun versions side by side
- Apply and test each change separately
It also suggested arrayBuffer() for binary operations, which I probably wouldn’t have reached for on my own.
What Didn’t Transfer Cleanly
Not every Node package worked with Bun. Documentation was thinner, and Bun’s errors sometimes gave me less to work with than Node’s. Native C and C++ extensions were the biggest compatibility risk.
Test the dependencies that matter Don’t assume Node compatibility means your production path works. Check native extensions and the code behind your critical requests before changing runtimes.
The Result
Build time dropped from 37 seconds to 20 seconds. Cold starts fell from 1.2 seconds to 0.3 seconds. Those waits were short, but I hit them often enough to notice.
| What changed | Node.js | Bun | Result |
|---|---|---|---|
| Cold start | 1.2s | 0.3s | 75% faster |
| Build | 37s | 20s | 46% faster |
| Dependencies | 20+ | 15 | 25% fewer |
| TypeScript | Compile first | Run directly | One less step |
I also ended up with fewer configuration files and more direct file-system code. That matters more to me than winning a runtime benchmark.
How I Would Do It Again
I wouldn’t migrate everything in one pass. The order that worked was:
- Package management and simple scripts
- Build configuration
- CI
- File operations
- Dependency cleanup
That order kept each failure small enough to understand. For this I/O-heavy codebase, the migration paid for itself within the first week. I didn’t need to keep Node just because the project started there.
One quick signal
Did this earn your time?
Thanks. That gives me something concrete to check.



