Building Production-Ready REST APIs with Bun: A Complete Guide
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
└── .envArchitecture 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<Response>
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)
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);
};One of Bun’s killer features is built-in password hashing:
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": "john.doe@example.com",
"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": "john.doe@example.com",
"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 installConfigure Environment:
Edit .env with your database credentials
Create Database:
CREATE DATABASE bun_app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;Development Mode
bun run devThis runs the server with hot-reloading via — watch flag.
Production Mode
bun run startBuild for Production
bun run buildCode 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/
Comments
Post a Comment