Back to the blog
BunJavaScriptNode.jsArchitecture

Bun on a long-running project: the lesson from a Minecraft city

October 04, 2026·6 min read·Diego Horvatti

Mike Tomlin, an NFL coach, spent 12 years building a city in Minecraft. Twelve years. Block by block. The story ran in The Athletic, and I read it thinking about something that has nothing to do with football: the way we adopt tools like Bun in projects that will live for a long time.

Sounds like a stretch? A little. But stay with me.

What an NFL coach has to do with your backend

Nobody builds a 12-year city in a weekend. You lay down a street. Then a house. Then you notice the street came out crooked and you redo just that stretch. The city never stops working while you change it.

Software that lasts is exactly the same. The system you maintain today probably has code from three or four different "eras". There's a chunk in CommonJS, another in ESM. There's Jest, and a shell script nobody remembers writing. And there are people using it in production right now, while you read this.

That's the setting where the urge to swap everything for Bun shows up. It's fast, runs TypeScript directly, and ships with a test runner, a bundler, a package manager, SQLite and a Postgres client. The temptation to go "big bang" is huge.

Don't.

Why rewriting everything in Bun is the worst strategy

A full rewrite has a well-known problem: for weeks you have two systems, and neither is finished. The old one keeps getting fixes. The new one is always behind. When you finally flip the switch, that bug shows up. The one that only happened with a weird combination of a native dependency and an environment variable.

And Bun has a detail that matters here: Node compatibility is very good, but it's not 100%. Packages with native addons, things that rely on specific behavior of internal node: modules, tools that read process.versions and pick a path based on it. In most projects, nothing happens. But "most" isn't "yours".

Nobody builds a whole city at once. Or migrates a backend.

The sensible way is Tomlin's: one block at a time, with the city running the whole time.

How to adopt Bun block by block

This is the order I use and recommend. Every step is reversible. If something goes wrong, you go back one step and move on.

Block 1: just the package manager

The cheapest step. You swap npm install for bun install and keep Node as the runtime.

rm -rf node_modules package-lock.json
bun install

Bun generates a text-based bun.lock, which you can review in a PR without pain. In CI, install time usually drops noticeably, especially in a monorepo with lots of dependencies. Your code didn't change a single line. If something goes wrong, you restore the old lockfile and that's it.

Block 2: the tests

bun test accepts a Jest-style API: describe, it, expect, mock. In many cases, your tests run without changes.

import { describe, it, expect } from 'bun:test'
import { calculateShipping } from './shipping'

describe('calculateShipping', () => {
  it('makes shipping free above 300', () => {
    expect(calculateShipping({ total: 350, zipCode: '01001000' })).toBe(0)
  })
})

This is where you start feeling the difference day to day. A test suite that's slow to start is one nobody runs before committing. A fast one becomes a habit.

A tip: start with a single folder. Run bun test src/utils and see what breaks. Usually it's module mocks or some more exotic jest.useFakeTimers. Fix it and expand.

Block 3: scripts and internal tools

You know that database seed script? The one that migrates data? The one that generates reports? Those are perfect candidates. Nobody outside touches them, a failure won't take down production, and the gain from running .ts directly without ts-node or a build is immediate.

// scripts/seed.ts
import { sql } from 'bun'

const users = await Bun.file('./fixtures/users.json').json()

for (const u of users) {
  await sql`insert into users (name, email) values (${u.name}, ${u.email})`
}

console.log(`${users.length} users inserted`)

Run it with bun scripts/seed.ts. No config. No new dependency.

Block 4: a small, new service

Only now does Bun become the runtime for something in production. And it's not your monolith. It's a new service with a tight scope: a webhook, a queue worker, an internal API.

Bun.serve({
  port: 3000,
  routes: {
    '/health': new Response('ok'),
    '/webhook': {
      POST: async (req) => {
        const event = await req.json()
        await processEvent(event)
        return new Response(null, { status: 204 })
      },
    },
  },
})

If that service runs for a few months with no surprises, then you have real data to discuss switching the runtime for everything else.

Block 5: the rest, if it makes sense

Notice the "if". Some projects stop at block 2 and are doing great. Fast installs and fast tests already pay for the effort. There's no medal for using Bun everywhere.

What to check before putting Bun in production

A few things I check before shipping a service on Bun:

  • Native dependencies. Run bun install and see if any package complains about building. bcrypt, sharp and old database drivers are the usual suspects. For password hashing, Bun.password does the job with no extra package.
  • Docker image. Use the official oven/bun image and pin the version. "latest" in production is asking for a surprise on a Friday afternoon.
  • Observability. Make sure your APM and your logger work on Bun. Some agents instrument at the Node runtime level and simply see nothing.
  • Deploy platform. Several platforms already accept Bun as a runtime today. Even so, test the deploy in a preview environment first.

None of this is a reason not to use it. It's just the equivalent of surveying the land before placing the first block.

So what's the lesson from 12 years of Minecraft?

I imagine Tomlin's city has neighborhoods built in different eras. One part in the style he had when he started, another with techniques he learned later. And that's fine. The city works as a whole precisely because he never tried to tear it all down and start over.

A repository is the same thing. Having a Bun service next to a Node one isn't a mess. It's a city under construction. The problem only shows up when nobody knows why each block is there. That's why it's worth leaving a short README or an ADR explaining the decision: "this worker runs on Bun because X, and the rest stays on Node because Y".

And there's a human bonus. Small changes are easier to review, easier to teach the team and easier to defend in a meeting. Nobody approves "let's switch the runtime for everything". Almost everyone approves "let's make CI faster by swapping the install".

My take on Bun today

Bun is past the toy phase. I use it every day and I'm not going back for installs and tests. As a production runtime, I use it for new services without fear, and for legacy systems only after measuring.

My strong opinion is this: Bun's biggest risk isn't technical, it's the excitement. A fast tool makes you want to rewrite everything. And a rewrite driven by excitement usually ends with two half-finished systems.

Do it like the coach: one block at a time, the city always standing. A few years from now you'll look back and realize you migrated almost everything without losing a single night of sleep. If you want to see how I've been applying this in my own projects, take a look at what I'm building.

LinkedIn summary

An NFL coach spent 12 years building a city in Minecraft. Block by block, without ever tearing it all down.

I thought about that when I saw the urge every team gets to rewrite the whole backend in Bun.

Bun is fast and ships with a test runner, a bundler and SQLite. That's exactly why the excitement is the biggest risk.

For me it works in this order: first bun install, then the tests, then the internal scripts, and only then a small new service in production.

Every step can be undone. And some projects stop at the second one and are doing just fine.

I wrote the full step by step on the blog, with the checklist I use before shipping to production. If you're thinking about migrating, it's worth a read.

#Bun #JavaScript #TypeScript #NodeJS #SoftwareDevelopment