-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
221: Building a Recipe Finder App Using a Public API
I've seen this happen countless times with developers moving from local logic to real-world apps: they treat an API call like a standard JavaScript function. They think that if they call fetch(), the data just... appears in their variable, ready to be used. It's a frustrating wall to hit because the code looks logically sound, but the output is always Promise {<pending>}.
The myth: "fetch() returns my data"
Imagine you're building our Recipe Finder. You want to get a list of chicken recipes from TheMealDB API. You might write something like this:
const recipes = fetch('https://www.themealdb.com/api/json/v1/1/filter.php?i=chicken_breast');
console.log(recipes); // You expect a list of recipes, but you get a Promise object.
The reason this fails is that the internet is slow (relative to your CPU). JavaScript doesn't stop the entire browser from freezing while it waits for a server in another country to respond. Instead, fetch returns a Promise—a placeholder that says, "I'll let you know when the data actually gets here." If you try to map over recipes immediately, your app will crash because you can't iterate over a Promise.
The reality: The two-step resolution process
To actually get your recipes, you have to await the response, and then—this is the part people often forget—you have to tell JavaScript how to read that response. The raw response from a server is just an HTTP stream; it isn't a JavaScript object yet. You have to parse it as JSON.
async function getRecipes(ingredient) {
const response = await fetch(`https://www.themealdb.com/api/json/v1/1/filter.php?i=${ingredient}`);
// This second 'await' is where the actual data extraction happens
const data = await response.json();
return data.meals; // TheMealDB wraps results in a 'meals' array
}
I always tell my mentees to think of it like ordering a pizza. fetch is the act of placing the order. The Response object is the delivery driver arriving at your door. But you can't eat the pizza while it's still inside the cardboard box—.json() is the act of opening the box so you can actually access the food.
Connecting the API to your UI
Now that we have the data, we can't just dump a JSON array into the HTML. We need to transform that data into something a human wants to see. In a Recipe Finder, this usually means looping through the array and generating "recipe cards."
I prefer using .map() combined with .join('') to create a big string of HTML. It's much cleaner than appending elements one by one in a forEach loop.
async function displayRecipes() {
const searchInput = document.querySelector('#search-box').value;
const recipes = await getRecipes(searchInput);
const container = document.querySelector('#recipe-grid');
if (!recipes) {
container.innerHTML = '<p>No recipes found for that ingredient!</p>';
return;
}
container.innerHTML = recipes.map(meal => `
<div class="recipe-card">
<img src="${meal.strMealThumb}" alt="${meal.strMeal}">
<h3>${meal.strMeal}</h3>
<a href="recipe-details.html?id=${meal.idMeal}">View Recipe</a>
</div>
`).join('');
}
Dealing with the "Empty State"
One detail that separates a junior project from a professional app is how it handles failure. What happens if the user searches for "unobtainium"? The API won't throw an error; it will simply return an object where meals is null.
If you try to call .map() on null, your entire app will white-screen. I've included a simple if (!recipes) check in the code above. Never trust the API to always give you a valid array. Always verify the data exists before you try to render it to the DOM.
📋 Practical Task
Implementing a "Random Recipe" Discovery Button
Your Recipe Finder currently relies on user input, but users often don't know what they want. Your task is to add a "Surprise Me" feature.
Requirements:
- Add a button to your HTML with the ID
#random-btn. - Create a function that fetches a single random meal from this endpoint:
https://www.themealdb.com/api/json/v1/1/random.php. - When the button is clicked, clear the current recipe grid and display only that one random recipe card.
- Ensure you handle the asynchronous nature of the request using
async/await. - Bonus: Add a loading spinner or a "Searching..." text to the grid while the random recipe is being fetched.
There are no comments for now.