My teammate Bryan asked to migrate the project from Node.js to Bun. I said sure. I expected to spend an afternoon fighting it. I didn’t, and that surprised me more than the speed numbers.
What we were running
The project is a POS system for small businesses. The JavaScript side of it is a Turbo monorepo: a Next.js 15 frontend in apps/web, a shared types package in packages/api-types, and a config package in packages/config. The backend is Go and sits outside this entirely. Before the migration, the root package.json looked like this:
{
"name": "malla",
"private": true,
"workspaces": ["apps/web", "packages/*"],
"devDependencies": {
"turbo": "^2.5.0"
}
}
No packageManager field. npm was the default. npm install at the root resolved the workspace graph, linked the local packages, and installed everything into a shared node_modules. Nothing exotic. The kind of setup you copy from a template and don’t think about again.
What the migration actually was
Two things changed. First, the packageManager field in the root package.json:
{
"packageManager": "bun@1.2.19"
}
Second, package-lock.json was replaced by bun.lock. That’s it. No dependency changes. No script rewrites. No configuration untangling. Bun is compatible enough with the Node ecosystem and the npm registry that the migration was mostly a declaration, not a rewrite.
bun install reads package.json the same way npm does. It resolves workspace packages by the same workspaces field. It produces a lockfile in a different format, but the resolution logic is the same. If your project was clean on npm, it will be clean on Bun.
What could have broken
TypeScript projects are where Bun compatibility is most often questioned. The concern is usually around module resolution: Node’s default CommonJS versus ESM, the various moduleResolution settings in tsconfig.json, and whether Bun handles edge cases the same way.
In this project the TypeScript configuration is standard Next.js 15, which ships with "moduleResolution": "bundler". Bun supports this. The cross-package imports from @malla/api-types into apps/web resolved exactly as before, because Bun reads the exports field in package.json the same way the bundler resolution algorithm expects.
The one thing to watch in a Turbo monorepo is the task pipeline. Turbo itself doesn’t care what package manager you use: it reads turbo.json, caches based on file hashes, and delegates the actual installs and script execution to whatever the package manager is. Since we didn’t change turbo.json, the pipeline stayed identical.
What actually changed
Install time dropped from ~40 seconds to under 4. Dev server startup became fast enough that I stopped looking away from the terminal while it came up. The test runner, which was using a custom Bun test setup Bryan had defined, ran noticeably faster under load.
None of this is surprising if you’ve read the Bun benchmarks. Bun is written in Zig, uses a custom JavaScript engine instead of V8, and its package manager is designed from scratch rather than layered on top of existing infrastructure. The performance advantage is architectural, not incidental.
What I wasn’t expecting was the quietness. Node has a way of reminding you it exists: the cold starts, the TypeScript loader friction before you wire up ts-node or tsx, the moments where the resolution algorithm does something unexpected and you spend twenty minutes reading GitHub issues to understand why. Those are small costs individually. Across weeks of daily development they add up.
Bun so far has not done that. I run bun install, bun dev, bun test. They finish and I do the work. The tooling does not interrupt the thinking.
What it does not fix
The hard parts of the project are still hard. The offline sync engine for sales made without connectivity, the Go API, the BCV exchange rate feed for Venezuelan payment handling, the idempotency guarantees in the order flow: none of that changed. Bun is a runtime and package manager, not an architecture. It makes the infrastructure layer faster and quieter. What you build on top of it is still entirely up to you.
It also does not fix Node compatibility issues if you have them. If your project depends on packages that use Node APIs in ways Bun doesn’t yet support, you will find out immediately on install or first run. We didn’t have any of those, but it’s the first thing to check before migrating a project with a heavy dependency tree.
Whether to migrate your project
If your monorepo runs on npm or yarn and you haven’t touched the tooling in a while, the migration is probably a single afternoon and the cost is low. The compatibility is good enough that most standard setups just work. Declare the packageManager field, delete the old lockfile, run bun install, run your tests, ship the updated bun.lock.
The return is not dramatic on any single day. It is the kind of improvement that compounds over time: slightly faster feedback loops, slightly less friction on every install and test run, slightly less mental overhead from the tooling. Over months of daily development that is worth more than any single feature you might have shipped instead.
The migration cost us one afternoon. We have not thought about it since.
