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

12 Best Websites to Practice Coding for Beginners

Using Generic in TypeScript about types in the Code

Solution: Codeforces Round #827 (Div. 4) Editorial