Bun.js Complete Guide — TypeScript Runtime Faster Than Node.js 2024

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Introduction

Why This Matters

Bun is not just another JavaScript runtime — it is a complete reimagining of the Node.js toolchain. Bun combines a runtime (replacing Node.js), a package manager (replacing npm/pnpm), a bundler (replacing esbuild/webpack), and a test runner (replacing Vitest/Jest) into a single binary with no configuration required.

For TypeScript backend developers, Bun's most significant advantage is that it runs TypeScript files natively — no compilation step, no ts-node, no tsx. You write .ts files and execute them directly. In benchmarks, Bun's HTTP server handles 3-4x more requests per second than Node.js with similar code.

In 2024, Bun is production-ready and increasingly adopted for new greenfield projects, serverless functions, and anywhere startup time and throughput are critical.

Installation

# Install Bun
curl -fsSL https://bun.sh/install | bash
 
# Verify installation
bun --version
 
# Create a new project
mkdir my-api && cd my-api
bun init -y
 
# Run TypeScript directly
bun run src/index.ts

Bun HTTP Server

Bun's built-in HTTP server is faster than both Node.js HTTP and Express.

// src/index.ts — no framework needed for simple APIs
const server = Bun.serve({
  port: 3000,
  hostname: '0.0.0.0',
 
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);
 
    if (url.pathname === '/health' && request.method === 'GET') {
      return Response.json({ status: 'ok', timestamp: new Date().toISOString() });
    }
 
    if (url.pathname === '/api/users' && request.method === 'GET') {
      const users = await db.query('SELECT id, name, email FROM users').all();
      return Response.json({ users });
    }
 
    if (url.pathname === '/api/users' && request.method === 'POST') {
      const body = await request.json() as { name: string; email: string };
      const result = db.run(
        'INSERT INTO users (name, email) VALUES (?, ?)',
        [body.name, body.email]
      );
      return Response.json({ id: result.lastInsertRowid }, { status: 201 });
    }
 
    return new Response('Not Found', { status: 404 });
  },
 
  error(error: Error): Response {
    console.error('[Server Error]', error);
    return new Response('Internal Server Error', { status: 500 });
  },
});
 
console.log(`Bun server running on http://localhost:${server.port}`);

Built-in SQLite

Bun ships with SQLite bindings — no native module compilation, no pg or mysql2 required for simple use cases.

import { Database } from 'bun:sqlite';
 
const db = new Database('app.db');
 
// Enable WAL mode for better concurrency
db.run('PRAGMA journal_mode = WAL');
db.run('PRAGMA synchronous = NORMAL');
 
// Create table
db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`);
 
interface User {
  id: number;
  name: string;
  email: string;
  created_at: string;
}
 
// Prepared statements for performance
const findUserById = db.prepare<User, [number]>('SELECT * FROM users WHERE id = ?');
const insertUser = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
const listUsers = db.prepare<User, []>('SELECT * FROM users ORDER BY created_at DESC');
 
// Usage
const user = findUserById.get(1);     // User | null
const allUsers = listUsers.all();     // User[]
const result = insertUser.run('Alice', 'alice@example.com');
console.log(result.lastInsertRowid); // number

For production APIs, use Hono with Bun.

import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { Database } from 'bun:sqlite';
 
const db = new Database('app.db');
const app = new Hono();
 
app.use('*', logger());
app.use('/api/*', cors());
 
const createUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});
 
app.post(
  '/api/users',
  zValidator('json', createUserSchema),
  async (c) => {
    const { name, email } = c.req.valid('json');
    const stmt = db.prepare('INSERT INTO users (name, email) VALUES (?, ?) RETURNING *');
    const user = stmt.get(name, email);
    return c.json(user, 201);
  }
);
 
export default {
  port: parseInt(process.env.PORT ?? '3000'),
  fetch: app.fetch,
};

File I/O

Bun's file API is considerably faster than Node.js's fs module.

// Read a file
const file = Bun.file('config.json');
const config = await file.json<{ port: number; host: string }>();
 
// Write a file
await Bun.write('output.txt', 'Hello from Bun!');
 
// Write JSON
await Bun.write('data.json', JSON.stringify({ key: 'value' }, null, 2));
 
// Stream a large file
const stream = Bun.file('large-file.csv').stream();
for await (const chunk of stream) {
  // process chunk
}

Built-in Test Runner

// src/users.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
import { Database } from 'bun:sqlite';
 
describe('User service', () => {
  let db: Database;
 
  beforeAll(() => {
    db = new Database(':memory:');
    db.run('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');
  });
 
  afterAll(() => db.close());
 
  it('inserts a user', () => {
    const stmt = db.prepare('INSERT INTO users (name, email) VALUES (?, ?) RETURNING *');
    const user = stmt.get('Alice', 'alice@example.com') as { id: number; name: string };
    expect(user.name).toBe('Alice');
    expect(user.id).toBeGreaterThan(0);
  });
});
# Run tests
bun test
 
# Watch mode
bun test --watch
 
# Coverage
bun test --coverage

Node.js Compatibility

Bun is ~95% compatible with the Node.js API and npm ecosystem.

// Works in Bun unchanged
import fs from 'fs/promises';
import path from 'path';
import { createServer } from 'http';
import crypto from 'crypto';
 
// npm packages work too
import express from 'express'; // works but slower than Bun.serve
import { z } from 'zod';       // works perfectly
import pg from 'pg';           // works for PostgreSQL

Common Mistakes

  • Using node:fs when Bun.file() is 5-10x faster for the same operation
  • Expecting 100% Node.js API compatibility — some native modules still need Node.js
  • Not using prepared statements in bun:sqlite — ad-hoc queries are slower
  • Using console.log for request logging instead of a structured logger
  • Not setting Bun.serve error handler — unhandled errors become empty 500 responses

Best Practices

  • Use bun:sqlite with WAL mode for local/embedded database needs
  • Use Hono or ElysiaJS as your framework — they are optimized for Bun's fetch-based server model
  • Use bun test instead of Vitest — the API is compatible and Bun's runner is faster
  • Use bun build for compiling TypeScript to a single executable for deployment
  • Profile with bun --smol for memory-constrained environments

Key Takeaways

  • Bun runs TypeScript natively — no compilation step or transpiler configuration required
  • Bun.serve() handles ~120,000 req/sec on typical hardware vs ~40,000 for Node.js + Express
  • bun:sqlite provides synchronous, high-performance SQLite access with no native module setup
  • Bun is a drop-in npm replacement — bun install is 20-100x faster than npm install
  • bun test is compatible with Jest and Vitest test APIs and requires zero configuration
  • Bun.file() reads and writes files 5-10x faster than Node.js fs.promises
  • Bun supports ~95% of the Node.js API — most Express and Fastify apps run without modification
  • bun build compiles TypeScript apps to standalone executables for serverless and edge deployment

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading