Building Production-Ready REST APIs with Bun: A Complete Guide
From Node.js to Bun: Embracing the Next Generation JavaScript Runtime
Introduction
The JavaScript ecosystem is witnessing a significant shift with the emergence of Bun – a blazingly fast all-in-one JavaScript runtime that challenges the decade-long dominance of Node.js. This article explores a production-grade REST API built entirely with Bun, demonstrating how this modern runtime can revolutionize your backend development workflow.
What is Bun?
Bun is a modern JavaScript runtime designed from the ground up, written in Zig and powered by JavaScriptCore (the same engine that powers Safari). Unlike Node.js which uses the V8 engine, Bun takes a different approach, focusing on:
Extreme Speed: Bun starts up to 4x faster than Node.js
All-in-One Toolkit: Built-in bundler, transpiler, package manager, and test runner
Native TypeScript Support: No additional configuration or tools needed
Built-in Security APIs: Native password hashing with bcrypt and argon2
Native File I/O: Faster file operations than Node.js
Web-Standard APIs: Implements Fetch, WebSocket, and other Web APIs
Bun vs Node.js: A Quick Comparison
JavaScript Engine: Node.js uses V8, while Bun leverages JavaScriptCore.
Language: Node.js is built with C++, whereas Bun is written in Zig for low-level performance.
TypeScript Support: Bun provides native support out of the box, unlike Node.js which requires ts-node or tsc.
All-in-One Tooling: Bun includes a built-in package manager, bundler, and test runner, replacing the need for external tools like npm, webpack, or Jest.
Startup Performance: Bun starts in 10-15ms, significantly faster than the 50ms typical of Node.js.
Native APIs: Features like HTTP servers (Bun.serve()) and password hashing (Bun.password) are native to Bun, whereas Node.js often requires external packages like bcrypt.
Project Overview: A Production-Grade Bun REST API
This article dissects a real-world Bun application – a User Management REST API built with enterprise-grade patterns and best practices. The application demonstrates how to build scalable, maintainable backend services using Bun.
Technology Stack
Bun (JavaScript Runtime): Chosen for its extreme speed, native TypeScript support, and comprehensive built-in toolset.
TypeORM (ORM Layer): A mature, decorator-based ORM that provides database-agnostic data mapping.
MySQL (Database): A reliable and widely-used relational database management system.
Zod (Validation): Provides type-safe runtime validation with seamless TypeScript inference.
dotenv (Configuration): Simplifies environment variable management across different environments.
Project Architecture
The application follows a clean, layered architecture that separates concerns and promotes maintainability:
bun-app/
├── src/
│ ├── config/ # Configuration files
│ │ ├── database.ts # TypeORM DataSource configuration
│ │ └── env.ts # Environment variables management
│ │
│ ├── controllers/ # Request/Response handling
│ │ └── user_controller.ts
│ │
│ ├── services/ # Business logic layer
│ │ └── user_service.ts
│ │
│ ├── models/ # Database entities (TypeORM)
│ │ └── user.ts
│ │
│ ├── routes/ # API endpoint definitions
│ │ ├── index_routes.ts
│ │ └── user_routes.ts
│ │
│ ├── middlewares/ # Request processing middleware
│ │ ├── cors.ts
│ │ ├── error_handler.ts
│ │ └── logger.ts
│ │
│ ├── schemas/ # Zod validation schemas
│ │ └── user_schema.ts
│ │
│ ├── utils/ # Helper functions
│ │ ├── password_utils.ts
│ │ ├── response_utils.ts
│ │ ├── validation_utils.ts
│ │ └── zod_utils.ts
│ │
│ ├── app.ts # Application setup
│ └── server.ts # Entry point
│
├── tests/ # Test files
├── package.json
├── tsconfig.json
└── .env
Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ CLIENT REQUEST │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Bun.serve() │
│ (Built-in HTTP Server) │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ MIDDLEWARE LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Logger │─▶│ CORS │─▶│ Error Handler │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ROUTING LAYER │
│ (Route Matching) │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ CONTROLLER LAYER │
│ (Request Parsing + Validation + Response) │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SERVICE LAYER │
│ (Business Logic + ORM) │
└───────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ DATABASE LAYER │
│ (MySQL via TypeORM) │
└─────────────────────────────────────────────────────────────────┘
Deep Dive: Core Components
1. Server Entry Point (server.ts)
The server is the heart of the application. Notice how Bun's Bun.serve() creates an incredibly efficient HTTP server:
import "reflect-metadata";
import { env } from "./config/env";
import { initialize_database } from "./config/database";
import { create_app } from "./app";
const start_server = async () => {
try {
// Initialize database connection
await initialize_database();
// Create and start the server
const app = create_app();
const server = Bun.serve({
port: env.port,
fetch: app.fetch,
});
console.log(`
╔════════════════════════════════════════════╗
║ 🚀 Server is running! ║║ Environment: ${env.node_env} ║
║ Port: ${env.port} ║
║ URL: http://localhost:${env.port} ║
╚════════════════════════════════════════════╝
`);
// Graceful shutdown
process.on("SIGINT", async () => {
console.log("\n🛑 Shutting down gracefully...);
server.stop();
process.exit(0);
});
} catch (error) {
console.error("❌ Failed to start server:", error);
process.exit(1);
}
};
start_server();"
Key Highlights:
Bun.serve(): Native HTTP server with exceptional performance
Graceful Shutdown: Proper cleanup on SIGINT/SIGTERM signals
Database Initialization: Ensures TypeORM connects before accepting requests
2. Application Setup (app.ts)
The application follows a functional composition pattern:
import { add_cors_headers, cors_middleware } from "./middlewares/cors";
import { error_handler } from "./middlewares/error_handler";
import { logger_middleware } from "./middlewares/logger";
import { handle_routes } from "./routes/index_routes";
export const create_app = () => {
return {
async fetch(request: Request): Promise<Response> {
try {
// Logger middleware
logger_middleware(request);
// CORS preflight
const cors_response = cors_middleware(request);
if (cors_response) {
return cors_response;
}
// Handle routes
const response = await handle_routes(request);
// Add CORS headers to response
return add_cors_headers(response, request);
} catch (error: any) {
const error_response = await error_handler(error, request);
return add_cors_headers(error_response, request);
}
},
};
};
Design Pattern Explained:
The create_app() function returns an object with a fetch method. This function:
Receives a Request object
Returns a Promise
This pattern aligns with the Web Standards Fetch API and is the interface that Bun.serve() expects. It's elegant, testable, and framework-agnostic.
3. Native Password Hashing (password_utils.ts)
One of Bun's killer features is built-in password hashing:
export const hash_password = async (password: string): Promise<string> => {
return await Bun.password.hash(password, {
algorithm: "bcrypt",
cost: 10,
});
};
export const verify_password = async (
password: string,
hash: string
): Promise<boolean> => {
return await Bun.password.verify(password, hash);
};
Why This Matters:
No External Dependencies: Unlike Node.js which requires bcrypt or bcryptjs
Native Performance: Written in Zig, compiled to native code
Security: Supports bcrypt and argon2 algorithms
Simplicity: Clean, promise-based API
4. TypeORM Entity Definition (models/user.ts)
The User model uses TypeORM decorators for schema definition:
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from "typeorm";
@Entity("users")
export class User {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ type: "varchar", length: 255, unique: true })
email: string;
@Column({ type: "varchar", length: 255 })
password: string;
@Column({ type: "varchar", length: 100 })
first_name: string;
@Column({ type: "varchar", length: 100 })
last_name: string;
@Column({ type: "boolean", default: true })
is_active: boolean;
@Column({ type: "enum", enum: ["user", "admin"], default: "user" })
role: "user" | "admin";
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
}
Notes:
UUID Primary Key: More secure than auto-increment integers
Automatic Timestamps: CreateDateColumn and UpdateDateColumn handled by TypeORM
Role Enum: Database-level constraint for user roles
5. Zod Validation Schemas (schemas/user_schema.ts)
Type-safe runtime validation with automatic TypeScript type inference:
import { z } from "zod";
// Password validation schema with custom rules
const password_schema = z
.string()
.min(8, "Password must be at least 8 characters long")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(/[a-z]/, "Password must contain at least one lowercase letter")
.regex(/[0-9]/, "Password must contain at least one number");
// Create user schema
export const create_user_schema = z.object({
email: z.string().email("Invalid email format").toLowerCase().trim(),
password: password_schema,
first_name: z.string().min(1).max(100).trim(),
last_name: z.string().min(1).max(100).trim(),
role: z.enum(["user", "admin"]).optional().default("user"),
});
// Type exports for TypeScript
export type create_user_input = z.infer<typeof create_user_schema>;
Benefits of Zod:
Type Inference: TypeScript types derived from runtime schemas
Data Transformation: Automatic trimming, lowercase conversion
Detailed Errors: User-friendly validation messages
Composability: Schemas can be reused and extended
6. Service Layer (services/user_service.ts)
The service layer encapsulates all business logic:
import { Repository } from "typeorm";
import { app_data_source } from "../config/database";
import { User } from "../models/user";
import { hash_password } from "@/utils/password_utils";
export class UserService {
private user_repository: Repository<User>;
constructor() {
this.user_repository = app_data_source.getRepository(User);
}
async create_user(data: {
email: string;
password: string;
first_name: string;
last_name: string;
role?: "user" | "admin";
}): Promise<User> {
const existing_user = await this.user_repository.findOne({
where: { email: data.email },
});
if (existing_user) {
throw new Error("User with this email already exists");
}
const hashed_password = await hash_password(data.password);
const user = this.user_repository.create({
email: data.email,
password: hashed_password,
first_name: data.first_name,
last_name: data.last_name,
role: data.role || "user",
});
return await this.user_repository.save(user);
}
async get_all_users(options?: {
skip?: number;
take?: number;
}): Promise<{ users: User[]; total: number }> {
const [users, total] = await this.user_repository.findAndCount({
skip: options?.skip || 0,
take: options?.take || 10,
order: { created_at: "DESC" },
});
return { users, total };
}
// ... additional methods
}
Design Principles:
Single Responsibility: Only handles user-related business logic
Dependency Injection Ready: Repository injected via constructor
Error Handling: Throws descriptive errors for edge cases
Pagination Support: Built-in skip/take for large datasets
7. Controller Pattern (controllers/user_controller.ts)
Controllers handle HTTP request/response logic:
export const create_user_controller = async (
request: Request
): Promise<Response> => {
try {
const body = await request.json();
// Validate request body using Zod schema
const validation = validate_schema(create_user_schema, body);
if (!validation.success) {
return validation_error_response(validation.errors);
}
const user = await user_service.create_user(validation.data);
const { password, ...user_without_password } = user;
return success_response(
user_without_password,
"User created successfully",
201
);
} catch (error: any) {
return error_response(error.message, 400);
}
};
Key Patterns:
Input Validation: All inputs validated before processing
Password Stripping: Passwords never returned in responses
Consistent Responses: Using utility functions for response formatting
8. Response Utilities (utils/response_utils.ts)
Standardized API responses:
export interface api_response<T = any> {
success: boolean;
message?: string;
data?: T;
error?: string;
errors?: Record<string, string[]>;
}
export const success_response = <T>(
data: T,
message?: string,
status: number = 200
): Response => {
const response_body: api_response<T> = {
success: true,
data,
};
if (message) {
response_body.message = message;
}
return new Response(JSON.stringify(response_body), {
status,
headers: { "Content-Type": "application/json" },
});
};
export const validation_error_response = (
errors: Record<string, string[]>
): Response => {
return error_response("Validation failed", 422, errors);
};
Benefits:
Consistency: All API responses follow the same structure
Type Safety: Generic typing for response data
HTTP Semantics: Proper status codes for different scenarios
API Endpoints
The application exposes a complete User Management API:
GET / or /health: Health check
POST /api/users: Create new user
GET /api/users: List all users (paginated)
GET /api/users/:id: Get user by ID
PUT /api/users/:id: Update user
DELETE /api/users/:id: Delete user
PATCH /api/users/:id/toggle-status: Toggle user active status
Sample API Request/Response
Create User Request:
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"password": "SecurePass123",
"first_name": "John",
"last_name": "Doe",
"role": "user"
}'
Success Response:
{
"success": true,
"message": "User created successfully",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "[email protected]",
"first_name": "John",
"last_name": "Doe",
"is_active": true,
"role": "user",
"created_at": "2026-02-01T10:30:00.000Z",
"updated_at": "2026-02-01T10:30:00.000Z"
}
}
Validation Error Response:
{
"success": false,
"error": "Validation failed",
"errors": {
"email": ["Invalid email format"],
"password": ["Password must be at least 8 characters long"]
}
}
Running the Application
Prerequisites
Install Bun (if not already installed):
# Linux/macOS
curl -fsSL https://bun.sh/install | bash
# Windows (via PowerShell)
powershell -c "irm bun.sh/install.ps1 | iex"
Install Dependencies:
bun install
Configure Environment:
cp .env.sample .env
# Edit .env with your database credentials
Create Database:
CREATE DATABASE bun_app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
Development Mode
bun run dev
This runs the server with hot-reloading via --watch flag.
Production Mode
bun run start
Build for Production
bun run build
Code Conventions
This project follows strict coding conventions:
Files: user_controller.ts, password_utils.ts
Functions: create_user(), get_all_users()
Variables: user_repository, hashed_password
Interfaces: env_config, api_response
Database Columns: first_name, created_at
All naming uses snake_case for consistency across:
File names
Function names
Variable names
Database columns
Interface/Type names
Security Features
The application implements multiple security layers:
Password Hashing: Bun's native bcrypt with cost factor 10
Input Validation: Comprehensive Zod schemas for all inputs
SQL Injection Protection: TypeORM's parameterized queries
CORS Configuration: Configurable origin restrictions
UUID Primary Keys: Non-sequential, unpredictable IDs
Password Exclusion: Passwords never returned in API responses
Performance Benefits
Why Bun Outperforms Node.js
Startup Time: Bun starts in ~10ms, making it 5x faster than Node.js (~50ms).
HTTP Throughput: Bun handles ~150,000 requests/sec, a 3x improvement over Node.js (~50,000).
Package Management: Bun installs dependencies in ~5s, which is 6x faster than Node.js (~30s).
TypeScript Support: Bun offers native execution, eliminating the compilation overhead required by Node.js.
Bun-Specific Optimizations in This Project
Native HTTP Server: Bun.serve() outperforms Express/Fastify
Built-in Password Hashing: No external crypto dependencies
Native TypeScript: Zero transpilation overhead
Optimized File I/O: Bun's file operations are significantly faster
When to Use Bun vs Node.js
Choose Bun When:
Building new greenfield projects
Performance is a critical requirement
You want native TypeScript support
You prefer an all-in-one toolkit
You want faster development cycles
Stick with Node.js When:
Using packages with native C++ addons (limited Bun support)
Requiring maximum ecosystem compatibility
Working on legacy projects with complex dependencies
Needing full AWS Lambda compatibility (improving but limited)
Conclusion
This Bun application demonstrates that building production-ready backend services with Bun is not only possible but often superior to traditional Node.js approaches. The combination of:
Blazing Fast Performance: Native speed with JavaScriptCore
Native TypeScript: No configuration, just write
Built-in Tools: Package manager, bundler, test runner
Web Standard APIs: Familiar, future-proof patterns
Clean Architecture: Separation of concerns with layered design
...makes Bun an compelling choice for modern JavaScript/TypeScript backend development.
The JavaScript runtime landscape is evolving. While Node.js remains a reliable workhorse, Bun represents the next generation – faster, simpler, and more integrated. Whether you're starting a new project or evaluating alternatives for performance-critical applications, Bun deserves serious consideration.
Resources
Bun Official Documentation: https://bun.sh/docs
TypeORM Documentation: https://typeorm.io/
Zod Documentation: https://zod.dev/
MySQL Documentation: https://dev.mysql.com/doc/















