•13 min read

Migrating from Node.js to Bun 1.2 in Production: Full-Stack HTTP, SQLite & Package Performance

Migrating from Node.js to Bun 1.2 in Production: Full-Stack HTTP, SQLite & Package Performance

This guide details the process of migrating existing Node.js microservices to Bun 1.2 for production deployment. We will cover HTTP server migration, bun:sqlite integration, WebSocket server implementation, npm package compatibility, native C++ addon considerations, Node.js API gap mitigation, and Docker containerization with performance benchmarks.

Bun 1.2 Core Advantages for Production

Bun 1.2 offers significant performance advantages over Node.js, primarily due to its underlying Zig implementation and JavaScriptCore engine. Key benefits include:

  1. Faster Startup Times: Critical for serverless functions and frequently scaled microservices.
  2. Reduced Memory Footprint: Lower operational costs and higher density per host.
  3. Integrated Tooling: bun install, bun run, bun test, and bun build streamline development workflows.
  4. Native APIs: bun:sqlite, bun:ffi, bun:serve provide highly optimized primitives.
Advertisement

HTTP Server Migration

Bun's native HTTP server (Bun.serve) is a high-performance alternative to Node.js's http module or frameworks like Express. It's designed for minimal overhead.

Node.js (Express) Example

// src/node-server.ts
import express from 'express';
import { readFileSync } from 'fs';
import path from 'path';

const app = express();
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;

app.use(express.json());

app.get('/', (req, res) => {
  res.send('Hello from Node.js Express!');
});

app.get('/data', (req, res) => {
  try {
    const data = readFileSync(path.join(__dirname, '../data.json'), 'utf8');
    res.json(JSON.parse(data));
  } catch (error) {
    console.error('Error reading data:', error);
    res.status(500).send('Internal Server Error');
  }
});

app.listen(port, () => {
  console.log(`Node.js Express server listening on port ${port}`);
});

Bun 1.2 (Bun.serve) Equivalent

Migrating to Bun.serve involves adapting request/response handling. Bun's Request and Response objects are Web-standard Request and Response objects.

// src/bun-server.ts
import { readFileSync } from 'fs';
import path from 'path';

const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;

const server = Bun.serve({
  port,
  async fetch(req: Request): Promise<Response> {
    const url = new URL(req.url);

    if (url.pathname === '/') {
      return new Response('Hello from Bun!', { status: 200 });
    }

    if (url.pathname === '/data') {
      try {
        const dataPath = path.join(import.meta.dir, '../data.json');
        const data = readFileSync(dataPath, 'utf8');
        return new Response(data, {
          headers: { 'Content-Type': 'application/json' },
          status: 200,
        });
      } catch (error) {
        console.error('Error reading data:', error);
        return new Response('Internal Server Error', { status: 500 });
      }
    }

    // Example for POST request with JSON body
    if (url.pathname === '/submit' && req.method === 'POST') {
      try {
        const body = await req.json();
        console.log('Received JSON body:', body);
        return new Response(JSON.stringify({ message: 'Data received', data: body }), {
          headers: { 'Content-Type': 'application/json' },
          status: 200,
        });
      } catch (error) {
        console.error('Error parsing JSON body:', error);
        return new Response('Bad Request: Invalid JSON', { status: 400 });
      }
    }

    return new Response('Not Found', { status: 404 });
  },
  error(error: Error): Response {
    console.error('Bun server error:', error);
    return new Response('Internal Server Error', { status: 500 });
  },
});

console.log(`Bun server listening on port ${server.port}`);

Note the use of import.meta.dir for path resolution, which is Bun's equivalent to Node.js's __dirname.

Native SQLite with bun:sqlite

Bun provides a highly optimized, native SQLite client via bun:sqlite. This eliminates the need for external npm packages like sqlite3 or better-sqlite3, which often involve native C++ addons and compilation steps.

Node.js (better-sqlite3) Example

// src/node-sqlite.ts
import Database from 'better-sqlite3';
import path from 'path';

const dbPath = path.join(__dirname, '../data.db');
const db = new Database(dbPath);

db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
  );
`);

export function insertUser(name: string, email: string) {
  const stmt = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
  const info = stmt.run(name, email);
  return info.lastInsertRowid;
}

export function getUsers() {
  const stmt = db.prepare('SELECT * FROM users');
  return stmt.all();
}

// Example usage
if (require.main === module) {
  insertUser('Alice', 'alice@example.com');
  insertUser('Bob', 'bob@example.com');
  console.log('Users:', getUsers());
  db.close();
}

Bun 1.2 (bun:sqlite) Equivalent

bun:sqlite offers a similar API surface, making migration straightforward.

// src/bun-sqlite.ts
import { Database } from 'bun:sqlite';
import path from 'path';

const dbPath = path.join(import.meta.dir, '../data.db');
const db = new Database(dbPath);

db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
  );
`);

export function insertUser(name: string, email: string) {
  const query = db.query('INSERT INTO users (name, email) VALUES (?, ?)');
  const result = query.run(name, email);
  // Bun's result object for INSERT operations might differ slightly
  // For SQLite, lastInsertRowid is often available on the database object or via PRAGMA
  // For simplicity, we'll assume a successful run implies insertion.
  // A more robust solution would query for the last_insert_rowid()
  const lastIdQuery = db.query('SELECT last_insert_rowid() as id;');
  return (lastIdQuery.get() as { id: number }).id;
}

export function getUsers() {
  const query = db.query('SELECT * FROM users');
  return query.all();
}

// Example usage
if (import.meta.main) {
  insertUser('Charlie', 'charlie@example.com');
  insertUser('David', 'david@example.com');
  console.log('Users:', getUsers());
  db.close();
}

Native WebSocket Servers

Bun's Bun.serve also includes first-class support for WebSockets, providing a high-performance, integrated solution.

Node.js (ws library) Example

// src/node-ws.ts
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', ws => {
  console.log('Client connected (Node.js WS)');
  ws.on('message', message => {
    console.log(`Received: ${message}`);
    ws.send(`Echo: ${message}`);
  });
  ws.on('close', () => console.log('Client disconnected (Node.js WS)'));
  ws.on('error', error => console.error('WebSocket error (Node.js WS):', error));
});

console.log('Node.js WebSocket server listening on port 8080');

Bun 1.2 (Bun.serve WebSocket) Equivalent

Bun's WebSocket API is integrated directly into Bun.serve, using a websocket configuration object.

// src/bun-ws.ts
const server = Bun.serve({
  port: 8080,
  fetch(req, server) {
    const url = new URL(req.url);
    if (url.pathname === '/ws') {
      const success = server.upgrade(req, {
        data: {
          // Optional: attach any data to the WebSocket connection
          connectedAt: Date.now(),
        },
      });
      if (success) {
        return; // Bun handles the WebSocket connection
      }
    }
    return new Response('Not Found', { status: 404 });
  },
  websocket: {
    open(ws) {
      console.log(`Client connected (Bun WS). Connected at: ${ws.data.connectedAt}`);
      ws.send('Welcome to Bun WebSocket!');
    },
    message(ws, message) {
      console.log(`Received: ${message}`);
      ws.send(`Echo: ${message}`);
    },
    close(ws, code, message) {
      console.log(`Client disconnected (Bun WS). Code: ${code}, Message: ${message}`);
    },
    error(ws, error) {
      console.error('WebSocket error (Bun WS):', error);
    },
    // Optional: perMessageDeflate, maxPayloadLength, idleTimeout
    // idleTimeout: 30, // seconds
  },
});

console.log(`Bun WebSocket server listening on port ${server.port}`);
Advertisement

npm Package Compatibility and Native C++ Addons

Bun aims for high compatibility with existing npm packages. Most pure JavaScript packages will work without modification.

Native C++ Addons

This is where compatibility can be challenging. Node.js native addons (.node files) are compiled against Node.js's N-API (Node-API) and V8 engine. Bun uses JavaScriptCore and its own FFI (Foreign Function Interface) for native interactions.

  • Direct Compatibility: Native C++ addons compiled for Node.js are generally not directly compatible with Bun.
  • Workarounds:
    • Pure JS Alternatives: Look for pure JavaScript alternatives to packages that rely on native addons.
    • bun:ffi: For critical performance-sensitive operations, you might need to rewrite the native logic using bun:ffi to call into a shared library (.so, .dylib, .dll) compiled specifically for Bun. This is a complex undertaking.
    • Containerization: If a specific native addon is indispensable and cannot be rewritten, you might consider running that specific microservice in a Node.js container and communicating with it via RPC. This negates some Bun benefits but ensures functionality.

Example: bcrypt (Native Addon)

bcrypt is a common package that uses native C++ addons for performance.

// src/node-bcrypt.ts
import bcrypt from 'bcrypt';

async function hashPassword(password: string) {
  const saltRounds = 10;
  const hashedPassword = await bcrypt.hash(password, saltRounds);
  console.log('Hashed password (Node.js):', hashedPassword);
  return hashedPassword;
}

async function verifyPassword(password: string, hash: string) {
  const isMatch = await bcrypt.compare(password, hash);
  console.log('Password match (Node.js):', isMatch);
  return isMatch;
}

if (require.main === module) {
  (async () => {
    const hash = await hashPassword('mysecretpassword');
    await verifyPassword('mysecretpassword', hash);
    await verifyPassword('wrongpassword', hash);
  })();
}

When running bun install bcrypt, Bun will attempt to install it. If it fails to build the native addon, you'll see an error. For such cases, you might need to use a pure JS alternative or a different approach.

Bun 1.2 (bcrypt with potential issues)

Bun can sometimes run Node.js native addons if they are pre-compiled for the correct architecture and N-API version, but this is not guaranteed and often fails. A safer approach is to use a pure JS alternative or a different library.

For bcrypt, a pure JS implementation like bcryptjs is often a viable fallback, though it will be slower.

// src/bun-bcryptjs.ts
import bcrypt from 'bcryptjs'; // Using bcryptjs for Bun compatibility

async function hashPassword(password: string) {
  const saltRounds = 10;
  const hashedPassword = await bcrypt.hash(password, saltRounds);
  console.log('Hashed password (Bun/bcryptjs):', hashedPassword);
  return hashedPassword;
}

async function verifyPassword(password: string, hash: string) {
  const isMatch = await bcrypt.compare(password, hash);
  console.log('Password match (Bun/bcryptjs):', isMatch);
  return isMatch;
}

if (import.meta.main) {
  (async () => {
    const hash = await hashPassword('mysecretpassword');
    await verifyPassword('mysecretpassword', hash);
    await verifyPassword('wrongpassword', hash);
  })();
}

Node.js API Gaps and Mitigation

While Bun aims for high Node.js compatibility, some APIs are not fully implemented or behave differently.

  • child_process: Most common functions like exec, spawn, fork are supported. Edge cases or specific options might differ.
  • vm module: Limited support. If your application heavily relies on vm.runInContext or similar, this might be a blocker.
  • domain module: Deprecated in Node.js, not supported in Bun.
  • cluster module: Bun does not have a direct equivalent for cluster. For multi-core utilization, you'd typically run multiple Bun processes and use a load balancer, or leverage Bun's built-in multi-threading for certain tasks (though not for HTTP server scaling in the same way cluster works).
  • fs module: Largely compatible, but some less common options or synchronous variants might have subtle differences. Always test file system heavy operations.
  • net / tls: Basic client/server functionality is present, but advanced configurations might require adjustments.

Mitigation Strategy:

  1. Comprehensive Testing: Unit, integration, and end-to-end tests are crucial to identify API gaps.
  2. Polyfills/Shims: For minor gaps, a small polyfill might be possible.
  3. Refactoring: For significant gaps, refactor the problematic code to use Web-standard APIs or Bun's native APIs.
  4. Feature Flags: Isolate problematic code paths with feature flags, allowing a gradual migration.

Docker Containerization Benchmarks

Containerizing Bun applications is straightforward. The official oven/bun Docker images provide a lean base.

Dockerfile for Node.js

# Dockerfile.node
FROM node:20-alpine AS base

WORKDIR /app

COPY package.json bun.lockb* ./
RUN npm install --frozen-lockfile

COPY . .

EXPOSE 3000
CMD ["node", "src/node-server.ts"]

Dockerfile for Bun

# Dockerfile.bun
FROM oven/bun:1.2.0-alpine AS base

WORKDIR /app

COPY package.json bun.lockb* ./
# Bun automatically installs dependencies on 'bun run' if not present,
# but explicit install is good practice for build stage.
RUN bun install --frozen-lockfile

COPY . .

EXPOSE 3000
CMD ["bun", "run", "src/bun-server.ts"]

Benchmarking Methodology

We'll compare image size and runtime memory usage for a simple HTTP server.

  1. Build Images:
    docker build -t node-app -f Dockerfile.node .
    docker build -t bun-app -f Dockerfile.bun .
    
  2. Image Size:
    docker images | grep "node-app\|bun-app"
    
  3. Runtime Memory (using docker stats):
    docker run -d --name node-server -p 3001:3000 node-app
    docker run -d --name bun-server -p 3002:3000 bun-app
    docker stats --no-stream node-server bun-server
    # Stop containers after collecting stats
    docker stop node-server bun-server && docker rm node-server bun-server
    

Benchmark Results (Illustrative)

MetricNode.js (20-alpine)Bun (1.2.0-alpine)Notes
Image Size~180 MB~100 MBBun's base image is significantly smaller.
Startup Time~500 ms~50 msBun is orders of magnitude faster.
Memory Usage~30 MB~10 MBFor a simple HTTP server, Bun uses less.
RPS (ab -c 50 -n 10000)~2500 RPS~7000 RPSBun.serve is highly optimized.

Note: These are illustrative benchmarks. Actual results depend heavily on application complexity, workload, and hardware.

Production Gotchas & Troubleshooting

  1. Error: Cannot find module '...':

    • Cause: Bun's module resolution is generally compatible but can differ for specific edge cases or if tsconfig.json paths are not correctly configured for Bun.
    • Fix:
      • Ensure tsconfig.json paths are correctly mapped and baseUrl is set.
      • Verify package.json exports field for imported packages.
      • Check for missing bun install or npm install if using a mix.
      • Bun's node_modules resolution can be stricter; ensure all dependencies are explicitly listed.
      • If using require(), ensure the file extension is present (e.g., require('./module.js')).
  2. Native Addon Failures:

    • Cause: Attempting to use a Node.js native C++ addon (.node file) directly with Bun.
    • Fix:
      • Identify the problematic package.
      • Search for a pure JavaScript alternative (e.g., bcryptjs instead of bcrypt).
      • If no alternative, consider isolating that functionality into a separate Node.js microservice or reimplementing with bun:ffi (advanced).
  3. process.env Differences:

    • Cause: Bun's process.env is largely compatible, but some Node.js-specific environment variables might not be present or behave identically.
    • Fix:
      • Explicitly define required environment variables in your deployment environment.
      • Avoid relying on highly Node.js-specific internal environment variables.
  4. Buffer API Inconsistencies:

    • Cause: While Bun supports Buffer, its underlying implementation might have subtle differences from Node.js's, especially with older or less common Buffer methods.
    • Fix:
      • Prioritize Web-standard Uint8Array and TextEncoder/TextDecoder where possible.
      • Thoroughly test code paths involving Buffer manipulation.
  5. fs.watch / fs.watchFile Behavior:

    • Cause: File system watching can be platform-dependent and have different performance characteristics or event triggers between Node.js and Bun.
    • Fix:
      • Test file watching extensively in your target production environment.
      • Consider external file watching solutions if native behavior is inconsistent.
  6. Bun.serve vs. Node.js HTTP Server keepAliveTimeout:

    • Cause: Bun's Bun.serve has an idleTimeout for WebSockets and a general connection timeout. Node.js has keepAliveTimeout. Default values and behavior might differ.
    • Fix:
      • Explicitly configure idleTimeout in Bun.serve for WebSockets.
      • For HTTP, ensure your load balancer or proxy handles connection timeouts gracefully, or implement custom timeout logic within your fetch handler if needed.

Frequently Asked Questions

  1. Q: Can I use existing Node.js frameworks like Express or NestJS with Bun?

    • A: Yes, largely. Bun aims for high Node.js compatibility, so many frameworks will run. However, you won't get the full performance benefits of Bun.serve for HTTP handling. For optimal performance, migrate HTTP endpoints to Bun.serve directly or use a Bun-native framework. bun install will typically handle framework dependencies.
  2. Q: How does Bun handle TypeScript compilation in production?

    • A: Bun has a built-in TypeScript transpiler. You can directly run .ts files with bun run src/index.ts. For production, bun build can compile your TypeScript into JavaScript, which can then be run. This eliminates the need for tsc or ts-node.
  3. Q: What about database drivers other than SQLite (e.g., PostgreSQL, MySQL)?

    • A: For most relational databases, you'll use existing npm packages (e.g., pg, mysql2, sequelize, prisma). These are typically pure JavaScript or rely on standard network protocols, so they generally work well with Bun. bun install will manage their dependencies.
  4. Q: Is bun:ffi a viable replacement for all native Node.js addons?

    • A: bun:ffi is powerful but requires significant effort. You need to write C/C++/Rust code, compile it into a shared library (.so, .dylib, .dll), and then define the function signatures in TypeScript for bun:ffi to call. It's a solution for performance-critical, isolated native logic, not a drop-in replacement for complex Node.js addons.
  5. Q: How do I debug a Bun application in production?

    • A: Bun supports the Chrome DevTools Protocol. You can start Bun with --inspect or --inspect-brk (e.g., bun --inspect src/index.ts) and connect a debugger (like VS Code's debugger or Chrome DevTools). This provides a familiar debugging experience similar to Node.js.
Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement