Case Study: Recipe Browser
Follow along with code examples here!
Setup
Overview
This case study application displays recipes from the https://dummyjson.com/recipes API, allowing users to click on a recipe card to see more details about it. They can also search for recipes using the provided form.

This case study application demonstrates DOM manipulation, fetching with .then()/.catch(), fetching with async/await, ES Modules, event delegation, dataset, and form handling. The completed solution is in src-solution/.
Explore the Solution
The completed solution is in src-solution/. Use the exercises below to investigate how the code works before building it from scratch.
Trace the Flow
For each user experience, trace the path through the code across files to explain how it works. In order of execution, write down the sequence of function calls:
where it was called
a brief description of what it does
what was returned.
Assume there are no errors unless specified. A "sequence diagram" may be drawn to better illustrate the flow.
An example is provided for the first scenario.
Scenario 1: The page loads and recipes are rendered on the screen

Sequence Flow:
main.js: On page load,getRecipes()is called.fetch-helpers.js:getRecipes()callsfetch(...), checksresponse.ok, extracts JSON data, and returns a promise resolving torecipes.main.js:.then((recipes) => ...)receives the resolved value and callshideError()andrenderRecipes(recipes).dom-helpers.js:renderRecipes()clears the old list, updates the count, creates recipe<li>cards, and appends them to#recipes-list.
Scenario 2: A user clicks a recipe card and the recipe details appear
Answer
main.js: The click handler on#recipes-listruns.event.target.closest('li')finds the clicked card and gives usli.main.js:getRecipeById(li.dataset.recipeId)is called with the card’s stored id.fetch-helpers.js:getRecipeById()callsfetch(...), checksresponse.ok, extracts JSON data, and returns a promise resolving torecipe.main.js:.then((recipe) => ...)receives the recipe updates the DOM withhideError()andrenderRecipeDetails(recipe).dom-helpers.js:renderRecipeDetails()shows#recipe-details, clears old content, builds the new details elements, and appends them.
Scenario 3: A user submits the search form and matching recipes are listed
Answer
main.js: The form submit handler runs, callsevent.preventDefault(), and readssearchTermplusisQuick(.checked).main.js:await getRecipeBySearchTerm(searchTerm)is called with the search term.fetch-helpers.js:getRecipeBySearchTerm()callsfetch(...), checksresponse.ok, extracts JSON data, and returns a promise resolving to a{ data, error }object.main.js: Afterawait, if there is no error and results exist, it optionally filters recipes to <= 20 minutes whenisQuickis true.main.js: It then callshideError()andrenderRecipes(recipes).dom-helpers.js:renderRecipes()clears old cards, updates the count, and appends the new search results.
Scenario 4: Search fails and an error message is shown
Answer
main.js: The submit handler callsawait getRecipeBySearchTerm(searchTerm).fetch-helpers.js: The fetch fails (network failure or!response.ok), so thecatchblock returns{ data: null, error }.main.js: The resolved value is{ data: null, error }, soif (error)is true andrenderError(...)is called.renderRecipes(...)is not called.dom-helpers.js:renderError()removeshiddenand sets error text, so the error message appears.The message stays visible because there is no timeout-based hide. It is only cleared later when a success branch explicitly calls
hideError().
Guided Reading Questions
Open each file and answer the questions.
index.html
index.htmlWhat does
type="module"on the<script>tag enable?Find the fallback recipe card in the
ul. What will happen to it once JavaScript loads successfully?Which elements start with
class="hidden"? Why would we hide them by default?What
data-attribute is on the fallback<li>? What value does it have?
Answers
It enables ES modules in the browser, so we can use
import/exportin JavaScript files.It gets removed when
renderRecipesruns, becauserecipesList.innerHTML = ''clears the list before new cards are appended.#recipe-detailsand#error-messagestart hidden so details and errors only appear when relevant user actions or failures occur.The fallback card has
data-recipe-id="1".
src-solution/fetch-helpers.js
src-solution/fetch-helpers.jsWhat does
getRecipesresolve to if the fetch succeeds? What does it resolve to if the fetch fails?What kind of errors does checking
response.okhandle that.catch()does not handle on its own?The API returns an object like
{ recipes: [...], total: 50, ... }. IngetRecipes, where is just the recipe array extracted, and what would break if we returned the full object instead?getRecipeByIdandgetRecipesfollow the same pattern. What is the one structural difference between them? Why doesn'tgetRecipeByIdneed a second.then()?Compare and contrast
getRecipeBySearchTermwith the other fetch helpers. What are the benefits/tradeoffs of usingasync/await+try/catchand returning{ data, error }? Which style of handling promises do you prefer?
Answers
It resolves to
data.recipes(an array) on success, and resolves tonullon failure.It handles HTTP failure responses (like 404/500) that do not reject
fetchby default..catch()alone only handles rejected Promises (network/throw errors).It is extracted in the second
.then((data) => { return data.recipes; }). If we returned the full object, code expecting an array (likerenderRecipes(recipes)andrecipes.length/forEach) would break.getRecipeshas an extra.thento extractdata.recipes;getRecipeByIddoes not because that endpoint already returns a single recipe object directly.async/awaitcan be easier to read and debug for sequential logic, and{ data, error }gives a consistent result shape. Tradeoff: it introduces a different return contract from the other helpers (null), so callers must handle two patterns. It is best to stick to one pattern so choose your preference!
src-solution/dom-helpers.js
src-solution/dom-helpers.jsWhy does
renderRecipesclear#recipes-listbefore rendering?Why does
renderRecipeDetailsremove thehiddenclass?What is the difference between
renderErrorandhideError?What does
li.dataset.recipeId = recipe.idadd to the DOM, what is that value used for later, and why store it on each card?
Answers
To remove old/fallback content before rendering new results and avoid duplicate cards.
The details section is hidden by default, so removing
hiddenmakes the selected recipe details visible.renderError(msg)shows the error element and sets text.hideError()clears text and hides it.It adds a
data-recipe-idattribute on eachli. Later, the click handler readsli.dataset.recipeIdto callgetRecipeById(...)with the id of the clicked item. Storing it on each card keeps the card tied to its API ID for event-driven fetching.
src-solution/main.js
src-solution/main.jsWhat are the three actions that can trigger a fetch in this file?
Where is event delegation used, and why?
Where is the search form handled, and why is the handler
async?Where does the quick filter (
Under 20 Minutes) apply?In which branches is
hideError()called?
Answers
Initial page load (
getRecipes), clicking a recipe card (getRecipeById), and submitting the search form (getRecipeBySearchTerm).On
#recipes-listclick. One parent listener handles clicks on dynamically rendered cards, including clicks on child elements viaclosest('li').In the
submithandler for#search-form; it isasyncbecause it awaitsgetRecipeBySearchTerm(...).In the search success branch, after fetch resolves: if
isQuickis true, results are filtered by total prep + cook time <= 20.In three success branches: after successful initial load, after successful recipe-details fetch, and after successful search before rendering results.
Building from Scratch
So, how could you build this application from scratch?
The process for creating an interactive and data-driven user interface typically follows this order:
Create the HTML with
idandclassattributes so we can target elements. Leave empty containers for content generated with JavaScript/DOM manipulation.Create fetch helper functions and test with console logs.
Create rendering helper functions. Data in -> DOM out.
Connect the data source to rendering logic. This can look like:
Page load -> fetch -> render
User click -> fetch -> render
Form submit -> extract form data -> fetch -> render
For each feature below, you'll see this pattern repeating itself!
Feature 1: Fetch and Render Recipes
Step 0: Tour the HTML {.unlisted .unnumbered}
We've taken care of the HTML for you. Walk through the provided files before writing JavaScript. Pay attention to the empty containers and the elements with ids that we use in our JavaScript.
index.html- Take note of:The hardcoded fallback recipe card in the
ul(what users see before JS loads or if fetch fails).The
#recipe-detailssection withclass="hidden"(hidden by default, shown on click).The
#error-messageparagraph withclass="hidden".The search form (
#search-form) withsearchTermtext input andisQuickcheckbox.
styles.css- Take note of the class.hidden { display: none !important; }.src/main.js- Starter file is empty.
Step 1: Create src/fetch-helpers.js - fetch a list of recipes {.unlisted .unnumbered}
src/fetch-helpers.js - fetch a list of recipes {.unlisted .unnumbered}These next 4 steps walk through implementing the first feature: fetching and rendering a list of recipes.
Skills:
fetch,.then(),.catch(),response.ok, named exports, returning from.then()
Create the file and write getRecipes:
Key Details:
fetch()returns a Promise.Check
response.okbecause 404/500 do not automatically reject fetch.response.json()returns a Promise and must be returned.We extract
data.recipesfrom the API response object.On failure, return
nullso callers can handle errors.
Step 2: Import and test in main.js {.unlisted .unnumbered}
main.js {.unlisted .unnumbered}Skills: named imports with
.jsextension,.then()on returned Promise
You should see an array of recipe objects in the console.
Step 3: Create src/dom-helpers.js - render the recipe list {.unlisted .unnumbered}
src/dom-helpers.js - render the recipe list {.unlisted .unnumbered}Skills:
document.createElement,append,dataset,innerHTML = '', named exports
Step 4: Wire up rendering in main.js {.unlisted .unnumbered}
main.js {.unlisted .unnumbered}Skills: module imports, null checking
We've now completely implemented the first feature: fetching and rendering all recipes!
Step 6: Add Error Rendering {.unlisted .unnumbered}
This last step adds useful feedback for the user when errors occur.
Skills: error state rendering, explicit UI state management
Add to dom-helpers.js:
Then use these in main.js:
Call
renderError(...)when fetch or search fails.Call
hideError()in success branches (after successful page-load fetch, recipe-details fetch, and successful search) so errors are dismissed manually when the app recovers.
For example:
Feature 2: Event Delegation to Render Recipe Details
Step 0: Tour the HTML {.unlisted .unnumbered}
We've taken care of the HTML for you. Walk through the provided files before writing JavaScript. Pay attention to the empty containers and the elements with ids that we use in our JavaScript.
index.html- Take note of:The
#recipe-detailssection withclass="hidden"(hidden by default, shown on click).The
data-recipe-idattribute on the fallback list item
styles.css- Take note of the class.hidden { display: none !important; }.
Step 1: Add getRecipeById to fetch-helpers.js {.unlisted .unnumbered}
getRecipeById to fetch-helpers.js {.unlisted .unnumbered}These next three steps walk through implementing the second feature: fetching recipe details when we click on a specific recipe.
Skills: template literals, shared fetch pattern
Step 2: Add the click handler with event delegation {.unlisted .unnumbered}
Skills: event delegation,
closest(),dataset, second fetch + render
Step 3: Add renderRecipeDetails to dom-helpers.js {.unlisted .unnumbered}
renderRecipeDetails to dom-helpers.js {.unlisted .unnumbered}Skills: showing hidden content, nested list rendering
This completes the second feature: clicking on a list item to fetch its details.
Feature 3: Search
Step 0: Tour the HTML {.unlisted .unnumbered}
We've taken care of the HTML for you. Walk through the provided files before writing JavaScript. Pay attention to the empty containers and the elements with ids that we use in our JavaScript.
index.html- Take note of:The search form (
#search-form) withsearchTermtext input andisQuickcheckbox.
Step 1: Add getRecipeBySearchTerm with async/await {.unlisted .unnumbered}
getRecipeBySearchTerm with async/await {.unlisted .unnumbered}These next two steps walk through implementing the final feature: searching for recipes.
Skills:
async/await,try/catch, standardized{ data, error }return object
Add this new helper in fetch-helpers.js:
Why this differs from the other fetch helpers:
It demonstrates the
async/await+try/catchstyle.It returns an object with
{ data, error }so callers can consistently inspect both success and failure fields (dataanderror).
Step 2: Add the search form handler in main.js {.unlisted .unnumbered}
main.js {.unlisted .unnumbered}Skills: form submit handling,
.checkedfor checkboxes, async event handlers, conditional filtering
Concepts Checklist
By the end of this walkthrough, you have demonstrated:
Last updated