Home
/
Blog
/
Blog article

7/4/2026

I Built a SaaS with Bun 2. Here's What Blew My Mind (and What Didn't)

Dark themed developer setup with Bun runtime, code editor with SQL and routing code, and SaaS architecture visualization

October 2025 to May 2026. About six months of shipping.

Last year I decided to build a SaaS from scratch. Auth, billing, a dashboard, API, background jobs, the whole thing. The usual stack would have been Node, Express, some ORM, Vite on the frontend, a task queue, and a test framework. Bun 1.3 had just dropped, and I thought: let me see how far one runtime can take me.

Six months later, here is what actually mattered.

What Blew My Mind

1. bun run instead of npm run is not a flex. It is a culture shift.

I know. Just faster npm. But the difference in feedback loop tightness when your dev server restarts in 4ms instead of 400ms is enormous. I stopped context-switching. I would write a route and save, and the terminal was already showing the new output before my fingers left the keyboard.

This compounds. Over hundreds of saves a day, the saved time is not the bottleneck. The saved attention is. You never lose flow waiting for a spinner.

2. Bun.SQL() replaces three dependencies

PostgreSQL for production. SQLite for tests. Same API. Same tagged template literals.

import { sql } from "bun";

// Works. No ORM. No driver. No connection pool config.
const users = await sql`SELECT * FROM users WHERE plan = ${plan}`;

I shipped the MVP with SQLite, zero setup. Then I switched to Postgres for production by changing a single connection string. No migration tooling needed. No Prisma, no Drizzle, no Knex. Bun handles the pool, the prepared statements, and the parameter binding.

Was I worried about SQL injection with template literals? No. Bun's SQL tagged templates auto-parameterize. It is safe by construction. The win: about 5,000 fewer node_modules files. Faster CI. No ORM learning curve.

3. Bun.serve() with builtin routing. No Express, no Hono, no Fastify.

I started prototyping without a router, expecting to add one later. I never did.

serve({
  routes: {
    "/*":             App,           // frontend
    "/api/users": {
      GET:  () => Response.json(users),
      POST: async (req) => { /* ... */ },
    },
    "/api/users/:id":  (req) => {
      const { id } = req.params;
      // ...
    },
  },
});

No app.get(), no router.post(), no middleware registration order bugs. The routing is declared inline, zero-cost if you don't use it, and supports path params, catch-alls, and per-method handlers natively.

The thing that surprised me: param validation is trivial because you are just handling strings. You reach for zod only when you actually need a schema, not because the router forces middleware on you.

4. The SQLite to Postgres symmetry

The real killer feature. I ran integration tests against SQLite, in-memory, nanosecond setup. Production against Postgres. The same sql tagged template calls work on both. My CI went from 3 minutes to 18 seconds.

// test
const db = new SQL("sqlite://:memory:");
await db`CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT)`;

// prod
const db = new SQL(process.env.DATABASE_URL!);
await db`SELECT * FROM users WHERE email = ${email}`;

No test containers. No Docker. No PgTmp. Just :memory:.

5. Hot reloading across frontend and backend

Bun 1.3's frontend dev server does HMR with React Fast Refresh out of the box. Backend reloads are instant.

bun ./index.html
# Bun v1.3.14 ready in 6.62ms
# -> http://localhost:3000/

One command. Running both the SPA and the API. No CORS issues because they share a port. Console logs from the browser appear in my terminal. It felt like cheating.

6. bun build --compile: shipping a single binary to a client

I needed to give a client an evaluation copy without npm install instructions. Bun's --compile produced a single 65MB statically-linked binary that included the full-stack app, the SQLite data, and the runtime.

bun build --compile ./index.html --outfile myapp
# -> myapp (65MB, self-contained)

The client ran ./myapp on a bare Ubuntu server with nothing installed and it just worked. The binary serves the frontend 1.8x faster than nginx serving the same static files.

What Didn't (Realities You Should Know)

1. Node.js compatibility is 95%. That 5% will bite you.

Most npm packages work. But when they don't, the error messages are often cryptic C++ stack traces from Bun's internals. You can't Google those.

The offenders:

  • pg (the npm package): use Bun.sql() instead. The npm pg driver mostly works but has edge cases with large result sets.
  • ioredis: use Bun.redis() instead. Same story.
  • get-port: Bun already has the port system handled. This package throws inscrutable errors.
  • Anything with native addons: bcrypt, sharp, node-canvas. Sharp now works since Bun v1.2, bcrypt sorta works, others still hit issues.

The rule: if there is a Bun-native API for something, use it. If there isn't, test the npm package before committing.

2. The ORM ecosystem has not caught up

If your SaaS requires Prisma or Drizzle for complex migrations or schema management by non-engineers, Bun is not ready for you yet. Prisma's engine is a Rust binary that doesn't always play nice with Bun's runtime. Drizzle works but has occasional edge cases with Bun's connection pooling.

If you are comfortable with raw SQL, and with Bun's sql tagged templates it is genuinely pleasant raw SQL, you will be fine. If you need a migration framework, roll your own with bun sql files.

3. Debugging is worse

Node has decades of debugging tooling. Chrome DevTools, --inspect-brk, VS Code launch configs. Bun's inspector works but:

  • Variables don't always resolve in the debugger
  • Async stack traces are now good since v1.3.12, but were terrible before
  • No node --watch equivalent that works with debug mode. Bun's --watch and --inspect together sometimes crash

For daily development, console.log and --watch are faster anyway. But for that one impossible bug, I missed Node's debugger.

4. Background job libraries are limited

BullMQ, the Redis-backed job queue, runs on Bun but has subtle timing bugs. I ended up writing a simple job queue using Bun.redis() and Bun.cron() which was added in v1.3.12 for periodic tasks.

// In-process cron, no separate worker process needed
import { cron } from "bun";

cron.schedule({
  pattern: "0 * * * *",
  task: async () => {
    await processInvoices();
  },
});

Good enough for an MVP. For serious job queues with retries, backpressure, and worker pools, I would still reach for Node and BullMQ.

5. Ecosystem documentation assumes Node

Every time you Google how to do X with Bun, you get Node.js answers that mostly work but have subtle differences. Bun's docs are excellent for what they cover, but they cover maybe 40% of what a production SaaS needs. The rest you figure out by trial, error, and reading Bun's GitHub issues.

The Bottom Line

Would I build another SaaS with Bun today? Yes.

Do it if:

  • You are greenfielding with no legacy Node deps
  • You are comfortable with raw SQL
  • Your team is small enough that debugging tooling rough edges are tolerable
  • You value iteration speed over ecosystem maturity

Don't do it if:

  • You need Prisma or Drizzle for complex migrations
  • You are using 10+ obscure npm packages with native addons
  • You have a large team and need battle-tested debugging workflows
  • Your architecture depends on complex job queues

Bun 1.3 and 1.4, what many are calling Bun 2 in spirit, is the most impressive JS runtime I have ever used for building. The dev experience is genuinely transformative. Faster feedback loops, fewer dependencies, simpler architecture.

For my next SaaS? I am starting with Bun and only reaching for Node when Bun proves insufficient. That day hasn't come yet.