Approach Comparison: Node.js + Express vs. Bun + ElysiaJS
For over a decade, spinning up a JavaScript backend meant reaching for Node.js paired with Express or Fastify. That stack still powers countless production systems. However, modern projects often turn into a fragile puzzle of separate tools for TypeScript transpilation (tsx/esbuild), environment loading (dotenv), test runners (jest/vitest), and schema validators (zod).
Bun collapses these separate tools into a single native binary powered by JavaScriptCore. When you combine Bun with ElysiaJS—a web framework engineered specifically around Bun’s HTTP primitives—you get sub-15ms cold starts, native TypeScript execution, and static type inference. In synthetic benchmarks and real-world I/O workloads alike, Elysia routinely handles over 150,000 requests per second with less than 60MB of baseline memory, compared to Express hovering around 20,000 req/s.
Pros and Cons of Switching to Bun and ElysiaJS
Pros
- Raw Throughput & Low Latency: Bun executes HTTP handling via low-level C++ bindings, yielding up to 4x to 8x higher requests per second than Express.
- Zero-Config TypeScript: Execute
.tsand.tsxfiles directly without wrestling withtsconfig.jsonpath mappers or build steps. - Native TypeBox Validation: Schema compilation runs through TypeBox JIT compilation, validating payloads up to 100x faster than standard runtime libraries like Joi.
- End-to-End Type Safety with Eden Treaty: Export your server type definition and consume fully typed API endpoints on your frontend, complete with autocomplete and compile-time error checks.
Cons
- Node.js Edge-Case Compatibility: While Bun supports most npm packages, legacy libraries that rely on undocumented V8 internals or older C++ native addons can still hit edge-case bugs.
- Middleware Ecosystem: The Elysia plugin ecosystem covers auth, OpenAPI, CORS, and rate limiting, but niche third-party plugins remain smaller than the decade-old Express catalog.
Recommended Setup and Prerequisites
You only need Bun installed locally to follow along. Bun provides official binaries for macOS, Linux, and Windows Subsystem for Linux (WSL).
# Install Bun on Linux / macOS
curl -fsSL https://bun.sh/install | bash
# Verify your installation (version 1.1 or later recommended)
bun --version
For code editing, Visual Studio Code with the official Elysia and Tailwind/TypeScript extensions offers seamless auto-completion and inline schema diagnostics.
Step-by-Step Implementation Guide
Step 1: Initialize the Project
Set up a clean project directory and install the core dependencies. Bun installs packages in roughly 50 to 200 milliseconds using global hard links:
mkdir bun-elysia-api
cd bun-elysia-api
bun init -y
bun add elysia @elysiajs/cors @elysiajs/swagger
Step 2: Build the Core Server and REST Endpoints
Create an index.ts file. We will define an in-memory product catalog with typed GET and POST routes, utilizing Elysia’s schema builder (t) for instant payload validation.
import { Elysia, t } from 'elysia';
import { swagger } from '@elysiajs/swagger';
import { cors } from '@elysiajs/cors';
interface Product {
id: string;
name: string;
price: number;
inStock: boolean;
}
const products: Product[] = [
{ id: '1', name: 'Mechanical Keyboard', price: 120, inStock: true },
{ id: '2', name: 'Ergonomic Mouse', price: 65, inStock: false },
];
export const app = new Elysia()
.use(cors())
.use(swagger({
documentation: {
info: {
title: 'Product Catalog API',
version: '1.0.0',
},
},
}))
.get('/api/products', () => products)
.get(
'/api/products/:id',
({ params, error }) => {
const product = products.find((item) => item.id === params.id);
if (!product) {
return error(404, { message: 'Product not found' });
}
return product;
},
{
params: t.Object({
id: t.String(),
}),
}
)
.post(
'/api/products',
({ body, set }) => {
const newProduct: Product = {
id: (products.length + 1).toString(),
...body,
};
products.push(newProduct);
set.status = 201;
return newProduct;
},
{
body: t.Object({
name: t.String({ minLength: 2 }),
price: t.Number({ minimum: 0 }),
inStock: t.Boolean(),
}),
}
)
.listen(3000);
console.log(`Server running at http://${app.server?.hostname}:${app.server?.port}`);
export type App = typeof app;
Start your server with hot reloading enabled:
bun run --watch index.ts
Open http://localhost:3000/swagger in your browser. Elysia automatically turns your TypeBox schema constraints into an interactive Swagger UI with full request and response documentation.
Step 3: Connect Frontend Using Eden Treaty
Eden Treaty synchronizes backend route definitions with your frontend code without code generators, build scripts, or manual OpenAPI syncing. First, install Eden in your client workspace:
bun add @elysiajs/eden
Import the App type export into your client script and initialize the Treaty client:
import { treaty } from '@elysiajs/eden';
import type { App } from './index';
const client = treaty<App>('http://localhost:3000');
async function fetchAndCreateProducts() {
// 1. Fully typed GET call with autocomplete on URL paths
const { data: allProducts, error: listError } = await client.api.products.get();
if (listError) {
console.error('Failed to load products:', listError.value);
return;
}
console.log(`Loaded ${allProducts.length} products:`, allProducts);
// 2. Type-checked POST call: TypeScript flags invalid field types immediately
const { data: created, error: createError } = await client.api.products.post({
name: 'USB-C Docking Station',
price: 89,
inStock: true,
});
if (createError) {
console.error('Validation failed:', createError.value);
return;
}
console.log(`Created product ID: ${created.id}`);
}
fetchAndCreateProducts();
Step 4: Production Deployment with Docker
Containerizing Bun applications yields lean deployment artifacts. A typical Alpine-based Bun image weighs under 100MB and starts in roughly 20 milliseconds.
Create a Dockerfile in your project root:
# Multi-stage production build
FROM oven/bun:1-alpine AS base
WORKDIR /app
# Cache and install dependencies
COPY package.json bun.lock* bun.lockb* ./
RUN bun install --frozen-lockfile --production
# Copy source files
COPY . .
# Run container as non-root user for security
USER bun
EXPOSE 3000
ENV NODE_ENV=production
CMD ["bun", "run", "index.ts"]
Build the container image and test it locally:
docker build -t bun-elysia-api .
docker run -p 3000:3000 bun-elysia-api
You now have an end-to-end type-safe API that starts instantly, enforces strict schema validation at wire speed, and requires zero client-side boilerplate.

