Skip to content

Bun

Bun is the default runtime for Juice development. It runs TypeScript directly, starts in milliseconds, and includes built-in SQLite, file I/O, and WebSocket support. Zero adapter needed -- Bun.serve() accepts a fetch handler natively.

Server Entry

Bun.serve() takes a fetch handler directly. No adapter, no compatibility layer.

// server.ts
import { createRouter } from '@cmj/juice/runtime';
import manifest from './flight-manifest.json';

const handler = createRouter(manifest, {
  root: import.meta.url,
});

Bun.serve({
  port: 3000,
  fetch: handler,
});

console.log('Listening on http://localhost:3000');

Bun natively supports the Request/Response APIs that Juice is built on. The fetch handler is called directly with zero conversion overhead.

Why Bun is the Default

  • Fastest JavaScript runtime for server workloads. Bun's HTTP server is written in Zig and optimized for throughput and latency.
  • Built-in SQLite via bun:sqlite -- no external dependency needed for database access.
  • Built-in file I/O via Bun.file() and Bun.write() -- faster than Node's fs module.
  • Native WebSocket support in Bun.serve() -- no separate WebSocket library needed.
  • TypeScript runs directly -- no build step for the server, no ts-node, no transpilation.
  • Best cold start time for Juice: 43ms measured from process start to first request served.

Run and Deploy

Development

Use --hot for server-side hot module replacement. When you change a file, Bun reloads it without restarting the process.

bun --hot server.ts

Production

Build with Juice, then run the production server:

juice build && bun server.ts

Docker

A minimal Dockerfile for deploying Juice on Bun:

FROM oven/bun:1 AS base
WORKDIR /app

# Install dependencies
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile --production

# Copy app source and build
COPY . .
RUN bun run juice build

# Run
EXPOSE 3000
CMD ["bun", "server.ts"]

This image is typically under 150MB, compared to 300MB+ for equivalent Node.js images.

File System

Bun provides fast, ergonomic file I/O APIs:

// Reading files
const file = Bun.file('./data/config.json');
const text = await file.text();
const json = await file.json();

// Writing files
await Bun.write('./data/output.txt', 'Hello, world!');
await Bun.write('./data/config.json', JSON.stringify(data, null, 2));

File uploads from a server action:

async function handleUpload(formData: FormData) {
  'use server';
  const file = formData.get('file') as File;
  const path = `./uploads/${file.name}`;
  await Bun.write(path, file);
  return { success: true, path };
}

For static file serving, use Bun.serve's static option or add a middleware that maps URL paths to files on disk.

SQLite

Bun includes a built-in SQLite driver -- no external packages needed:

import { Database } from 'bun:sqlite';

const db = new Database('app.db');
db.run('CREATE TABLE IF NOT EXISTS tasks (id INTEGER PRIMARY KEY, title TEXT)');

const tasks = db.query('SELECT * FROM tasks').all();

See the SQLite integration page for sharing the database via context and using it across routes.

WebSockets

Bun.serve() supports WebSockets natively alongside the HTTP fetch handler. Juice's fetch handler and your WebSocket handler coexist on the same server:

import { createRouter } from '@cmj/juice/runtime';
import manifest from './flight-manifest.json';

const handler = createRouter(manifest, {
  root: import.meta.url,
});

Bun.serve({
  port: 3000,
  fetch(req, server) {
    // Upgrade WebSocket requests
    if (req.headers.get('upgrade') === 'websocket') {
      server.upgrade(req);
      return; // Bun handles the upgrade
    }
    // Everything else goes to Juice
    return handler(req);
  },
  websocket: {
    open(ws) {
      ws.send('Connected');
    },
    message(ws, message) {
      ws.send(`Echo: ${message}`);
    },
    close(ws) {
      // cleanup
    },
  },
});

Environment Variables

Bun automatically loads .env files -- no dotenv package needed. Access variables through either API:

// Both work identically
const dbUrl = process.env.DATABASE_URL;
const dbUrl = Bun.env.DATABASE_URL;

Bun loads .env, .env.local, and .env.production (or .env.development) automatically based on the NODE_ENV value.

Performance

MetricMeasured
Throughput33.5K req/s
Cold start43ms
Peak memory67MB

These numbers are for a Juice app with server-rendered React components, routing, and middleware. Your actual performance will depend on your route complexity and data fetching, but the baseline overhead from Juice + Bun is minimal.