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
└── .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:

  1. Receives a Request object
  2. 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 install

Configure 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 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:

  1. Password Hashing: Bun’s native bcrypt with cost factor 10
  2. Input Validation: Comprehensive Zod schemas for all inputs
  3. SQL Injection Protection: TypeORM’s parameterized queries
  4. CORS Configuration: Configurable origin restrictions
  5. UUID Primary Keys: Non-sequential, unpredictable IDs
  6. 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

  1. Native HTTP Server: Bun.serve() outperforms Express/Fastify
  2. Built-in Password Hashing: No external crypto dependencies
  3. Native TypeScript: Zero transpilation overhead
  4. 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

Comments

Popular posts from this blog