Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Making HTTP Requests with Bun's `fetch` API

Welcome to the final lesson of our introductory module on Bun.js. Having set up our project, learned to run and debug TypeScript files, and managed configuration with .env files, we're now ready to tackle the primary way our bot will interact with the outside world: through HTTP requests.

This lesson focuses on Bun's built-in fetch API. As an experienced front-end developer, you are intimately familiar with the fetch API from the browser. Bun brings this same standard, web-native API to the server-side, but with significant performance optimizations. We will explore how to use this powerful tool to make external API calls, a skill that is fundamental to communicating with the Telegram Bot API and the core of our ultimate goal.

1. Why fetch is Built-in and Why It Matters

In the Node.js ecosystem, making HTTP requests has historically required third-party libraries like axios or node-fetch. While Node.js has been incrementally adding native fetch support, it's a relatively recent and evolving feature. Bun, by contrast, was designed from the ground up with a fast, stable, and web-standard fetch implementation as a core feature.

The video below from Codevolution clearly articulates this difference.

Bun vs Node.js - Everything You Need To Know!

This segment explains the advantage of Bun's native support for web APIs like fetch compared to the historical reliance on external packages in Node.js.

Watch the section from Web APIs. It highlights how Bun provides a stable, built-in fetch API, eliminating the need for extra dependencies and ensuring better performance.

This native integration is not just about convenience; it's a key part of Bun's performance-first philosophy. The fetch implementation is written in fast, low-level Zig code, making it significantly faster than JavaScript-based solutions.

A benchmark comparing a simple "Hello world" fetch operation per second across Node.js, Deno, and Bun. This illustrates the performance advantage of Bun's native implementation.

2. Making Your First GET Request

The fetch function is available globally in Bun, so you don't need to import anything to start using it. Let's make a simple GET request to a public API to see it in action. We'll use JSONPlaceholder, a common fake API for testing.

The article "How to Handle HTTP Requests in Bun" from OneUptime is an excellent, comprehensive guide that we will refer to throughout this lesson.

How to Handle HTTP Requests in Bun

This article provides practical examples for using Bun's native fetch API, covering everything from basic requests to more advanced patterns.

Start by reading the introductory sections Why Use Bun and Getting Started. Then, focus on the section Basic GET Request. Pay close attention to the pattern of awaiting the fetch call and then awaiting the response.json() method, as well as the important step of checking the response.ok property.

Hands-on: Fetching Data

Let's put this into practice in our index.ts file.

  1. Replace the content of your index.ts with the following code.

  2. Note that since we're using await at the top level of the file, Bun will execute it directly.

    // index.ts
    
    console.log("Fetching a post from JSONPlaceholder...");
    
    try {
      const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    
      // Check if the request was successful (status code 200-299)
      if (!response.ok) {
        throw new Error(`HTTP error! Status: ${response.status}`);
      }
    
      // Parse the JSON body of the response
      const data = await response.json();
    
      console.log("Fetch successful!");
      console.log("Post Title:", data.title);
    
    } catch (error) {
      console.error("Fetch failed:", error);
    }
    
  3. Run the file from your terminal: bun index.ts.

You should see the title of the first post logged to your console. You have successfully made an external API call using Bun's native fetch.

3. Sending Data with POST Requests

While GET requests are for retrieving data, POST requests are for sending data to a server to create a new resource. This is the primary method you will use to send messages, commands, and other data to the Telegram Bot API.

To make a POST request, you provide a configuration object as the second argument to fetch, specifying the method, headers, and body. When sending structured data, the standard practice is to send a JSON string in the body and set the Content-Type header to application/json.

How to Handle HTTP Requests in Bun

This section of the OneUptime article demonstrates how to construct and send a POST request with a JSON payload.

Now, read the section on Making POST Requests, focusing specifically on the subsection Sending JSON Data. This example is directly applicable to how we will interact with most modern web APIs, including Telegram's.

4. A Glimpse into the Telegram API

Let's tie this all together. We can combine our knowledge of environment variables and POST requests to construct a real API call to the Telegram Bot API.

The basic endpoint for sending a message is:
https://api.telegram.org/bot<YOUR_BOT_TOKEN>/sendMessage

This endpoint expects a POST request with a JSON body containing at least a chat_id and the text you want to send.

Hands-on: Simulating a sendMessage Call

We don't have a real bot token or chat ID yet (we'll get those in the next module), but we can still write the code that performs the action. This will fail with a "Not Found" error from Telegram, but it perfectly demonstrates the mechanism.

  1. Modify your index.ts to use the BOT_TOKEN from your .env file and make a POST request.

    // index.ts
    
    // Validate that the BOT_TOKEN is set
    if (!Bun.env.BOT_TOKEN) {
      throw new Error("BOT_TOKEN must be provided in .env file!");
    }
    
    const botToken = Bun.env.BOT_TOKEN;
    const apiUrl = `https://api.telegram.org/bot${botToken}/sendMessage`;
    
    // The data we want to send to Telegram
    const payload = {
      chat_id: "123456789", // A placeholder chat ID
      text: "Hello from my Bun-powered bot!",
    };
    
    console.log("Attempting to send a message to the Telegram API...");
    
    try {
      const response = await fetch(apiUrl, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(payload),
      });
    
      const responseData = await response.json();
    
      if (!response.ok) {
        // Telegram provides error details in the JSON body
        throw new Error(`Telegram API Error: ${responseData.description}`);
      }
    
      console.log("Telegram API response:", responseData);
    
    } catch (error) {
      console.error("Failed to send message:", error);
    }
    
  2. Run the file: bun index.ts.

You will see an error, likely Telegram API Error: Not Found. This is expected and correct! The request was successfully sent and parsed, but Telegram responded that the bot token is invalid. This exercise confirms that you have all the tools necessary to communicate with the API.

5. Building for Robustness

For production-grade applications, you need more than just basic requests. Given your experience, you know the importance of robust error handling, timeouts, and debugging.

  • Comprehensive Error Handling: Network connections can fail, and APIs can return non-2xx status codes. A try...catch block handles network failures, while checking response.ok is crucial for handling application-level errors. The OneUptime article we've been using has an excellent section on creating a safeFetch wrapper with a custom HttpError class. While you don't need to implement it now, reviewing this pattern is highly recommended for professional development.
  • Timeouts: A request could hang indefinitely. The standard way to handle this is with an AbortController. You can see an implementation in the "Implementing Request Timeouts" section of the OneUptime article.
  • Debugging: When an API call isn't behaving as expected, seeing the raw request and response can be invaluable. Bun provides a non-standard but very useful verbose: true option for this.

Fetch

The official documentation provides details on Bun-specific features, including a powerful debugging option.

Read the section on Debugging. This verbose: true flag is a simple but powerful tool for inspecting the exact headers and status lines of your HTTP interactions.

Conclusion

This lesson completes our foundational tour of the Bun runtime. You are now equipped with the final core skill needed to build your Telegram bot: making external HTTP requests. You understand the advantages of Bun's native fetch and have practical experience making both GET and POST requests. Most importantly, you've seen exactly how this fetch API will be used to communicate with the Telegram API.

Key Takeaways:

  • Bun includes a fast, web-standard fetch API out of the box, requiring no external dependencies.
  • Making a request involves the global fetch() function, which returns a Promise that resolves to a Response object.
  • You must await the response and then await a method like response.json() or response.text() to read the body.
  • POST requests are configured with an options object specifying the method, headers (like Content-Type), and body (often a stringified JSON payload).
  • Robust applications require diligent error handling (checking response.ok and using try...catch) and considering patterns for timeouts and debugging.

With our Bun toolkit fully assembled, we are ready to leave the workshop and start building. In the next module, we will dive headfirst into the Telegram Bot API. Our very first lesson will guide you through creating your bot with BotFather, obtaining your official API token, and understanding the fundamental architecture of how Telegram communicates with bots.

Can't find a good explanation? Sign up and we'll make it for you

Sign up