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.