How to Work with Web APIs Like fetch, Request, and Response in Bun

Bun provides a native, browser-standard Fetch API implementation built in Zig that exposes global fetch, Request, Response, and Headers objects, with a JavaScript compatibility shim in src/js/thirdparty/node-fetch.ts that adds Node.js-specific conveniences while maintaining Web standards compliance.

The oven-sh/bun runtime ships with a high-performance, native implementation of the Web Fetch API, enabling browser-compatible HTTP requests without external dependencies. Unlike Node.js, which historically required polyfills or third-party packages like node-fetch, Bun exposes these APIs globally while providing a compatibility layer to ensure seamless integration with existing Node.js libraries.

Architecture of Bun's Fetch Implementation

Bun's Fetch API operates through two distinct layers: a core runtime written in Zig and a thin JavaScript shim that ensures Node.js ecosystem compatibility.

Native Runtime Layer in Zig

The low-level networking logic resides in src/bun.js/webcore/fetch.zig, which implements the global fetch function and native constructors for Response, Request, and Headers. This layer handles TLS, HTTP/2, connection pooling, and streaming directly within the runtime, exposing these capabilities to JavaScript via internal bindings loaded through $cpp("NodeFetch.cpp", "createNodeFetchInternalBinding"). The native constructors—WebResponse, WebRequest, and WebHeaders—provide the foundation for all HTTP operations.

JavaScript Compatibility Shim

The file src/js/thirdparty/node-fetch.ts re-exports the native objects while adding Node.js-specific utilities. The shim defines wrapper classes that extend the native implementations: class Response extends WebResponse and class Request extends WebRequest. These wrappers handle stream conversion, relaxed URL parsing, and the addition of methods like Headers.raw() for compatibility with legacy Node.js code. When you invoke fetch() in Bun, the shim forwards the call to nativeFetch.$call and patches the returned prototype to match the wrapper classes.

Working with the Response Object

The Response class in src/js/thirdparty/node-fetch.ts extends the native WebResponse to provide seamless integration with Node.js streaming libraries. When you access response.body, the shim lazily converts the underlying Web Streams API readable stream into a Node.js Readable stream using Readable.toWeb(stream). This automatic conversion allows existing Node.js libraries—such as form-data or file parsers—to consume Bun fetch responses without modification.

The shim also overrides the clone() method and adds Node-specific getters to ensure that response.body behaves identically to traditional Node.js HTTP responses while maintaining Web standards compliance for properties like status, statusText, and headers.

Handling Request Construction and URL Parsing

Bun's Request implementation relaxes URL validation to match node-fetch behavior. If you pass a string that is not a valid URL (such as a path without a protocol), the constructor in src/js/thirdparty/node-fetch.ts (lines 20-33) creates a temporary http://localhost/ placeholder and stores the original string in a private symbol. The public url getter retrieves this stored value, allowing relative paths and non-standard URLs to work transparently with middleware and routing libraries.

This approach ensures that code relying on node-fetch-style URL handling works in Bun without modification, while the underlying native implementation still operates on valid URL objects for actual network operations.

Practical Code Examples

The following examples demonstrate how to use Bun's Fetch API patterns, all of which execute through the native Zig implementation while benefiting from the JavaScript shim's compatibility features.

Simple GET Request

// fetch-get.ts
const response = await fetch("https://api.bun.sh/health");
console.log("Status:", response.status);           // → 200
console.log("Body:", await response.text());       // → JSON string

This example uses the global fetch function exposed by src/bun.js/webcore/fetch.zig and processed through the shim's exported wrapper (lines 48-55 of src/js/thirdparty/node-fetch.ts).

POST with JSON Payload

// fetch-post.ts
const payload = { message: "Hello from Bun!" };
const response = await fetch("https://api.bun.sh/echo", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload),
});

const data = await response.json();               // ← parses JSON automatically
console.log(data);                                // { message: "Hello from Bun!" }

The Request object is constructed internally by the shim, which forwards the init options directly to the native Bun.fetch binding.

Streaming Large File Uploads

// fetch-stream.ts
import { createReadStream } from "node:fs";

const stream = createReadStream("./large-file.bin");   // Node readable stream
const response = await fetch("https://api.bun.sh/upload", {
  method: "PUT",
  body: stream,                     // shim converts Node stream → Web ReadableStream
  headers: { "Content-Type": "application/octet-stream" },
});

console.log("Uploaded, server says:", await response.text());

The shim automatically detects Node.js streams (lines 44-48 of src/js/thirdparty/node-fetch.ts) and converts them to Web Streams using Readable.toWeb(stream) before passing them to the native fetch implementation.

Aborting Requests with AbortController

// fetch-abort.ts
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5_000); // 5 s timeout

try {
  const response = await fetch("https://slow.api/big", {
    signal: controller.signal,
  });
  console.log(await response.text());
} catch (e) {
  if (e.name === "AbortError") console.error("Request aborted");
}
clearTimeout(timeout);

The AbortError class is defined in the compatibility shim (lines 77-81) and matches the Web API specification for canceled requests.

Accessing Raw Headers

// fetch-raw-headers.ts
const response = await fetch("https://api.bun.sh/metadata");
const rawHeaders = response.headers.raw();   // ← returns { headerName: [value] }
console.log(rawHeaders);

The Headers.raw() method, implemented in the shim (lines 13-25), returns header values as arrays to maintain compatibility with node-fetch patterns.

Summary

  • Bun implements the Fetch API natively in Zig via src/bun.js/webcore/fetch.zig, exposing global fetch, Request, Response, and Headers objects without requiring external packages.
  • A compatibility shim in src/js/thirdparty/node-fetch.ts extends native WebResponse and WebRequest classes to add Node.js-specific features like stream conversion and relaxed URL parsing.
  • Automatic stream conversion allows you to pass Node.js Readable streams directly to fetch(); the shim handles translation to Web Streams internally.
  • Non-standard URLs are supported through private symbol storage in the Request constructor, ensuring compatibility with routing libraries that pass relative paths.
  • Raw header access via headers.raw() and proper AbortError handling ensure that existing node-fetch codebases migrate to Bun without modification.

Frequently Asked Questions

Does Bun use the same Fetch API as browsers?

Yes. Bun's implementation follows the Web Fetch API specification exactly, exposing identical global objects: fetch, Request, Response, Headers, and AbortController. The runtime implements these in src/bun.js/webcore/fetch.zig for performance, with a thin JavaScript layer in src/js/thirdparty/node-fetch.ts that ensures Node.js compatibility without breaking Web standards compliance.

Can I pass Node.js streams directly to Bun's fetch?

Absolutely. When you provide a Node.js Readable stream as the body parameter, the compatibility shim detects the stream type and automatically converts it to a Web Streams API ReadableStream using Readable.toWeb(stream). This happens transparently in src/js/thirdparty/node-fetch.ts (lines 44-48), allowing you to use fs.createReadStream() or other Node.js streaming utilities directly with fetch().

How does Bun handle invalid URLs in Request objects?

Bun's Request constructor in the compatibility shim (lines 20-33 of src/js/thirdparty/node-fetch.ts) implements relaxed URL parsing. If you provide a string that is not a valid absolute URL, the constructor creates a dummy http://localhost/ base URL internally while storing your original string in a private symbol. The public url property returns your original value, maintaining compatibility with libraries that pass relative paths or protocol-less URLs.

Where are the TypeScript definitions for Bun's Fetch API located?

The TypeScript definitions reside in packages/bun-types/fetch.d.ts within the oven-sh/bun repository. These definitions provide full type support for the Web-standard Fetch API as implemented in Bun, including generic type parameters for Response.json() and proper AbortSignal integration for request cancellation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →