Making API Calls with grammY and Bun's Fetch
Hello! In our last lesson, we put the finishing touches on our conversational bot, building robust flows that can handle branching, user cancellation, and timeouts. Your bot can now manage complex, stateful interactions gracefully. However, so far, it has operated in a self-contained universe.
This lesson marks a significant turning point. We will open your bot up to the vast world of external data and services. You will learn how to integrate a call to an external API within a grammY command handler, using Bun's highly optimized fetch API. This skill transforms your bot from a simple conversationalist into a powerful orchestrator capable of fetching real-time information, posting data to other platforms, and much more.
The API Integration Pattern
As a seasoned front-end developer, you are intimately familiar with the fetch API for communicating with backend services. The pattern for using it within a grammY bot is conceptually identical to how you'd use it in a web application. The key is that grammY's middleware and handlers are designed to work with async/await, making the integration of asynchronous operations like API calls seamless.
The fundamental workflow inside a command handler looks like this:
- Receive a command from the user (e.g.,
/fact). - Make an HTTP request to an external API endpoint using
await fetch(...). - Check the response status to ensure the request was successful.
- Parse the response body, typically from JSON into a JavaScript object using
await response.json(). - Extract the relevant data from the parsed object.
- Send a formatted reply to the user via
ctx.reply(...).
Let's implement a new command, /joke, that fetches a random programming joke from a public API. This will be a practical, step-by-step demonstration of the pattern. We'll use the Official Joke API, which requires no authentication.
Implementing the /joke Command
First, open your src/bot.ts file. We will add a new command handler that embodies the pattern described above.
// In src/bot.ts
// ... other imports
// Add the new command handler somewhere before bot.start()
bot.command("joke", async (ctx) => {
try {
// 1. Make the API call
const response = await fetch(
"https://official-joke-api.appspot.com/jokes/programming/random"
);
// This is what the raw Response object from fetch looks like
// before we parse it.
console.log(response);
// 2. Check for success
if (!response.ok) {
// If the API call fails, inform the user.
await ctx.reply("Sorry, I couldn't fetch a joke right now. Please try again later.");
// Log the error for debugging
console.error("API call failed:", response.status, response.statusText);
return;
}
// 3. Parse the JSON response
// The API returns an array with a single joke object.
const [jokeData] = await response.json();
// 4. Construct and send the reply
const joke = `${jokeData.setup}\n\n${jokeData.punchline}`;
await ctx.reply(joke);
} catch (error) {
// Handle network errors or other exceptions during fetch
console.error("Error fetching joke:", error);
await ctx.reply("An unexpected error occurred while fetching a joke.");
}
});
// ... rest of your bot setup and bot.start()
This handler is a complete, self-contained example of an API integration. Let's break down the key parts:
async (ctx) => {...}: We declare the handler asasyncto allow the use ofawaitinside it. grammY fully supports this.try...catchblock: This is crucial for resilience. Thefetchcall can fail due to network issues, DNS problems, or the remote server being down. Thecatchblock ensures your bot doesn't crash and can report the failure gracefully.const response = await fetch(...): Here, we use Bun's nativefetch. As you know from your front-end work, this returns aResponseobject.!response.ok: This is a standard check. If the HTTP status is not in the 200-299 range, we send a user-friendly error and log the details for our own debugging purposes.const [jokeData] = await response.json(): Theresponse.json()method parses the response body as JSON. Based on the API's documentation, we know it returns an array containing one object, so we use array destructuring[jokeData]to get it directly.
When you run fetch, the response variable holds a Response object. The image below shows what this object typically looks like when logged in a browser or Node.js-like console. It contains metadata like the status code, headers, and the body stream, which you then consume with a method like .json().

Integrating via an SDK
While direct fetch calls are powerful, many services provide SDKs (Software Development Kits) that offer a more convenient, typed interface to their APIs. These SDKs are essentially wrappers that handle the underlying fetch calls, authentication, and request formatting for you.
The following article demonstrates building a bot that integrates with the Google Gemini API using its official SDK. While the setup uses Node.js and npm, the grammY-specific code for making the API call within a handler is identical to how you would do it in Bun.
Building a Telegram bot with grammY - LogRocket Blog
This article provides an excellent example of integrating a more complex external service into a grammY bot. We'll look at two specific parts: a simple API call in a command handler and a more advanced one that involves fetching a file first.
First, read the section that implements the /start command. You can find it by searching for the code snippet bot.command('start', ...). Notice how await chat.sendMessage(prompt) is an asynchronous API call, just like our await fetch(...). The SDK handles the details, but the pattern within the grammY handler is the same. Next, skip down to the section titled "How to respond to voice messages". Read the code block under it, starting from bot.on('message:voice', ...). This example is particularly insightful. It first uses ctx.getFile() (a grammY helper) to get file metadata from Telegram, then constructs a URL, and then uses a direct fetch call to download the file from Telegram's servers. Finally, it sends that downloaded data to the Gemini API. This two-step process—fetching data from one source to use in a request to another—is a very common and powerful pattern in bot development.
The principles from the article reinforce our own implementation: the core of the task is to perform an async operation inside a handler and use its result to reply to the user. Whether you use a direct fetch call or an SDK method like chat.sendMessage depends on the service you're integrating with and your preference for abstraction versus direct control.
Conclusion
In this lesson, you've bridged the gap between your bot and the outside world. You now have the foundational skill to make your bot infinitely more capable by leveraging the countless APIs available on the web.
Key Takeaways:
- grammY handlers can be
async, which makes integratingfetchcalls or other asynchronous operations straightforward. - The standard pattern is to
awaitthe API call, parse the response, and use the data inctx.reply(). - Robust error handling using
try...catchfor network errors and checking the response status (response.ok) is essential for a reliable bot. - APIs can be consumed directly via
fetchor through dedicated SDKs, which often provide a more convenient wrapper.
The data we get back from APIs is often raw JSON and not always in a user-friendly format. In our next lesson, we will focus on taking that raw data and transforming it into well-structured, readable messages for the user, using formatting options like Markdown and HTML. We'll explore how to turn a blob of JSON into a clear, organized reply, much like the formatted table you can see in the image below.

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