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()andBun.write()-- faster than Node'sfsmodule. - 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.tsProduction
Build with Juice, then run the production server:
juice build && bun server.tsDocker
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
| Metric | Measured |
|---|---|
| Throughput | 33.5K req/s |
| Cold start | 43ms |
| Peak memory | 67MB |
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.