Skip to content

Bun Fetch API

Bun has a built-in Fetch API that follows web standards for sending HTTP requests. This chapter introduces Bun's network request functionality.

Basic Requests

GET Request

typescript
// Simplest GET request
const response = await fetch("https://api.example.com/users");
const data = await response.json();
console.log(data);

// Get text
const text = await fetch("https://example.com").then(r => r.text());

// Get binary data
const buffer = await fetch("https://example.com/image.png")
  .then(r => r.arrayBuffer());

POST Request

typescript
// Send JSON
const response = await fetch("https://api.example.com/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "John",
    email: "john@example.com",
  }),
});

const result = await response.json();
console.log(result);

Other HTTP Methods

typescript
// PUT request
await fetch("https://api.example.com/users/1", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "New Name" }),
});

// PATCH request
await fetch("https://api.example.com/users/1", {
  method: "PATCH",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "new@example.com" }),
});

// DELETE request
await fetch("https://api.example.com/users/1", {
  method: "DELETE",
});

Request Configuration

Complete Configuration Options

typescript
const response = await fetch("https://api.example.com/data", {
  // HTTP method
  method: "POST",
  
  // Request headers
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer your-token",
    "Accept": "application/json",
    "User-Agent": "Bun/1.0",
  },
  
  // Request body
  body: JSON.stringify({ data: "value" }),
  
  // Redirect handling
  redirect: "follow", // "follow" | "error" | "manual"
  
  // Credentials
  credentials: "include", // "omit" | "same-origin" | "include"
  
  // Cache mode
  cache: "no-cache",
  
  // Timeout (ms) - Bun specific
  timeout: 30000,
  
  // TLS configuration - Bun specific
  tls: {
    rejectUnauthorized: true,
  },
});

Request Headers

typescript
// Using Headers object
const headers = new Headers();
headers.set("Content-Type", "application/json");
headers.set("Authorization", "Bearer token");
headers.append("Accept-Language", "en-US");

const response = await fetch("https://api.example.com", { headers });

// Check response headers
console.log(response.headers.get("content-type"));
console.log(response.headers.get("x-request-id"));

// Iterate response headers
for (const [key, value] of response.headers) {
  console.log(`${key}: ${value}`);
}

Response Handling

Response Object

typescript
const response = await fetch("https://api.example.com/data");

// Response status
console.log("Status:", response.status);         // 200
console.log("Status text:", response.statusText); // "OK"
console.log("Success:", response.ok);            // true (200-299)

// Response URL
console.log("Final URL:", response.url);

// Response type
console.log("Type:", response.type);             // "basic" | "cors" | etc.

// Redirected
console.log("Redirected:", response.redirected);

Reading Response Body

typescript
const response = await fetch("https://api.example.com/data");

// JSON
const json = await response.json();

// Text
const text = await response.text();

// ArrayBuffer
const buffer = await response.arrayBuffer();

// Blob
const blob = await response.blob();

// FormData
const formData = await response.formData();

// Note: Response body can only be read once
// Clone first if you need to use it multiple times
const clone = response.clone();
const text1 = await response.text();
const text2 = await clone.text();

Streaming Read

typescript
const response = await fetch("https://example.com/large-file");

// Get readable stream
const reader = response.body?.getReader();

if (reader) {
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    console.log("Read chunk:", value.length, "bytes");
  }
}

Form and File Upload

FormData

typescript
const formData = new FormData();
formData.append("name", "John");
formData.append("age", "25");

const response = await fetch("https://api.example.com/submit", {
  method: "POST",
  body: formData,
  // No need to manually set Content-Type, fetch handles it automatically
});

File Upload

typescript
// Upload local file
const file = Bun.file("./document.pdf");

const formData = new FormData();
formData.append("file", file);
formData.append("description", "Important document");

const response = await fetch("https://api.example.com/upload", {
  method: "POST",
  body: formData,
});

console.log("Upload result:", await response.json());

Multiple File Upload

typescript
const formData = new FormData();

// Add multiple files
formData.append("files", Bun.file("./image1.png"));
formData.append("files", Bun.file("./image2.png"));
formData.append("files", Bun.file("./image3.png"));

const response = await fetch("https://api.example.com/upload-multiple", {
  method: "POST",
  body: formData,
});

Error Handling

Basic Error Handling

typescript
try {
  const response = await fetch("https://api.example.com/data");
  
  if (!response.ok) {
    throw new Error(`HTTP Error: ${response.status}`);
  }
  
  const data = await response.json();
  return data;
} catch (error) {
  if (error instanceof TypeError) {
    console.error("Network error:", error.message);
  } else {
    console.error("Request failed:", error);
  }
}

Timeout Handling

typescript
// Method 1: Use Bun's timeout option
const response = await fetch("https://api.example.com/slow", {
  timeout: 5000, // 5 second timeout
});

// Method 2: Use AbortController
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);

try {
  const response = await fetch("https://api.example.com/slow", {
    signal: controller.signal,
  });
  clearTimeout(timeout);
  return await response.json();
} catch (error) {
  if (error.name === "AbortError") {
    console.error("Request timeout");
  }
  throw error;
}

Canceling Requests

typescript
const controller = new AbortController();

// Start request
const fetchPromise = fetch("https://api.example.com/data", {
  signal: controller.signal,
});

// Cancel under some condition
setTimeout(() => {
  controller.abort();
  console.log("Request canceled");
}, 1000);

try {
  const response = await fetchPromise;
} catch (error) {
  if (error.name === "AbortError") {
    console.log("Request was canceled");
  }
}

Retry Mechanism

Simple Retry

typescript
async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  maxRetries = 3
): Promise<Response> {
  let lastError: Error | null = null;
  
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      
      if (response.ok) {
        return response;
      }
      
      // Server error, can retry
      if (response.status >= 500) {
        lastError = new Error(`Server error: ${response.status}`);
        continue;
      }
      
      // Client error, don't retry
      return response;
    } catch (error) {
      lastError = error as Error;
      console.log(`Attempt ${i + 1} failed:`, error);
      
      // Wait before retrying
      await Bun.sleep(1000 * (i + 1));
    }
  }
  
  throw lastError || new Error("Request failed");
}

// Usage
const response = await fetchWithRetry("https://api.example.com/data");

Exponential Backoff

typescript
async function fetchWithExponentialBackoff(
  url: string,
  options: RequestInit = {},
  maxRetries = 5
): Promise<Response> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      if (response.ok) return response;
      if (response.status < 500) return response;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
    }
    
    // Exponential backoff: 1s, 2s, 4s, 8s, 16s
    const delay = Math.min(1000 * Math.pow(2, i), 30000);
    const jitter = Math.random() * 1000;
    await Bun.sleep(delay + jitter);
  }
  
  throw new Error("Max retries exceeded");
}

HTTP Client Wrapper

API Client

typescript
class ApiClient {
  private baseUrl: string;
  private headers: Record<string, string>;

  constructor(baseUrl: string, headers: Record<string, string> = {}) {
    this.baseUrl = baseUrl;
    this.headers = {
      "Content-Type": "application/json",
      ...headers,
    };
  }

  setHeader(key: string, value: string) {
    this.headers[key] = value;
  }

  setToken(token: string) {
    this.headers["Authorization"] = `Bearer ${token}`;
  }

  private async request<T>(
    method: string,
    path: string,
    body?: unknown
  ): Promise<T> {
    const response = await fetch(`${this.baseUrl}${path}`, {
      method,
      headers: this.headers,
      body: body ? JSON.stringify(body) : undefined,
    });

    if (!response.ok) {
      const error = await response.json().catch(() => ({}));
      throw new ApiError(response.status, error.message || response.statusText);
    }

    return response.json();
  }

  get<T>(path: string): Promise<T> {
    return this.request<T>("GET", path);
  }

  post<T>(path: string, body: unknown): Promise<T> {
    return this.request<T>("POST", path, body);
  }

  put<T>(path: string, body: unknown): Promise<T> {
    return this.request<T>("PUT", path, body);
  }

  patch<T>(path: string, body: unknown): Promise<T> {
    return this.request<T>("PATCH", path, body);
  }

  delete<T>(path: string): Promise<T> {
    return this.request<T>("DELETE", path);
  }
}

class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = "ApiError";
  }
}

// Usage
const api = new ApiClient("https://api.example.com");
api.setToken("your-auth-token");

const users = await api.get<User[]>("/users");
const newUser = await api.post<User>("/users", { name: "John" });

Concurrent Requests

Promise.all

typescript
// Parallel API requests
const [users, posts, comments] = await Promise.all([
  fetch("https://api.example.com/users").then(r => r.json()),
  fetch("https://api.example.com/posts").then(r => r.json()),
  fetch("https://api.example.com/comments").then(r => r.json()),
]);

console.log("Users:", users.length);
console.log("Posts:", posts.length);
console.log("Comments:", comments.length);

Promise.allSettled

typescript
// Continue even if some fail
const results = await Promise.allSettled([
  fetch("https://api.example.com/users").then(r => r.json()),
  fetch("https://api.example.com/may-fail").then(r => r.json()),
  fetch("https://api.example.com/posts").then(r => r.json()),
]);

results.forEach((result, index) => {
  if (result.status === "fulfilled") {
    console.log(`Request ${index} succeeded:`, result.value);
  } else {
    console.log(`Request ${index} failed:`, result.reason);
  }
});

Limiting Concurrency

typescript
async function fetchWithConcurrency<T>(
  urls: string[],
  concurrency: number,
  fetcher: (url: string) => Promise<T>
): Promise<T[]> {
  const results: T[] = [];
  const executing: Promise<void>[] = [];

  for (const url of urls) {
    const promise = fetcher(url).then(result => {
      results.push(result);
    });

    executing.push(promise);

    if (executing.length >= concurrency) {
      await Promise.race(executing);
      executing.splice(
        executing.findIndex(p => p === promise),
        1
      );
    }
  }

  await Promise.all(executing);
  return results;
}

// Usage: max 3 concurrent requests
const urls = [
  "https://api.example.com/1",
  "https://api.example.com/2",
  "https://api.example.com/3",
  "https://api.example.com/4",
  "https://api.example.com/5",
];

const results = await fetchWithConcurrency(
  urls,
  3,
  url => fetch(url).then(r => r.json())
);

Proxy and TLS

HTTP Proxy

typescript
// Use environment variables
process.env.HTTP_PROXY = "http://proxy.example.com:8080";
process.env.HTTPS_PROXY = "http://proxy.example.com:8080";

const response = await fetch("https://api.example.com");

Custom TLS

typescript
const response = await fetch("https://internal-api.company.com", {
  tls: {
    // Self-signed certificate
    rejectUnauthorized: false,
    
    // Or specify CA
    ca: Bun.file("./certs/ca.pem"),
  },
});

Downloading Files

typescript
// Download and save file
async function downloadFile(url: string, savePath: string) {
  const response = await fetch(url);
  
  if (!response.ok) {
    throw new Error(`Download failed: ${response.status}`);
  }
  
  await Bun.write(savePath, response);
  console.log(`File saved to: ${savePath}`);
}

// Usage
await downloadFile(
  "https://example.com/large-file.zip",
  "./downloads/file.zip"
);

Download Progress

typescript
async function downloadWithProgress(url: string, savePath: string) {
  const response = await fetch(url);
  const contentLength = response.headers.get("content-length");
  const total = contentLength ? parseInt(contentLength) : 0;
  
  let downloaded = 0;
  const reader = response.body?.getReader();
  const chunks: Uint8Array[] = [];
  
  if (reader) {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      chunks.push(value);
      downloaded += value.length;
      
      if (total) {
        const percent = ((downloaded / total) * 100).toFixed(1);
        console.log(`Download progress: ${percent}%`);
      }
    }
  }
  
  // Merge and save
  const blob = new Blob(chunks);
  await Bun.write(savePath, blob);
}

Summary

This chapter covered:

  • ✅ Basic HTTP request methods
  • ✅ Request configuration and response handling
  • ✅ Form and file upload
  • ✅ Error handling and timeouts
  • ✅ Retry mechanism and concurrency control
  • ✅ HTTP client wrapper
  • ✅ File download

Next Steps

Continue reading Bundling to learn about Bun's built-in bundling tools.

Content is for learning and research only.