Tier 3 โ€” Upper-Intermediate

Recipe Manager

Store recipes with ingredients and steps, search by ingredient, and scale servings up or down. A real app with nested data.

๐Ÿ Python ๐ŸŸจ JavaScript

1. The Problem

Build a recipe manager that stores recipes (each with a name, servings, a list of ingredients with amounts, and steps). You can search recipes by ingredient and scale a recipe to a new number of servings, adjusting every ingredient amount proportionally.

Why this project? Working with nested data structures, searching inside arrays of objects, and proportional scaling โ€” the kind of data-shaping every real app does.

2. How to Think About This

A recipe is an object holding an array of ingredient objects ({ name, amount }). To search, keep any recipe that has at least one ingredient whose name contains the query โ€” a case-insensitive some() over the ingredients. To scale, compute the ratio of new servings to old, then map each ingredient to the same name with its amount multiplied by that ratio.

3. The Build

recipes.mjs
// Find recipes that use a given ingredient (case-insensitive).
export function searchByIngredient(recipes, ingredient) {
  const needle = ingredient.toLowerCase();
  return recipes.filter(r =>
    r.ingredients.some(i => i.name.toLowerCase().includes(needle))
  );
}

// Scale a recipe to a new serving count, adjusting every amount.
export function scale(recipe, newServings) {
  const ratio = newServings / recipe.servings;
  const ingredients = recipe.ingredients.map(i => ({
    name: i.name,
    amount: Math.round(i.amount * ratio * 100) / 100,
  }));
  return { name: recipe.name, servings: newServings, ingredients, steps: recipe.steps };
}

// Demo
const pancakes = {
  name: "Pancakes",
  servings: 2,
  ingredients: [{ name: "flour", amount: 100 }, { name: "milk", amount: 200 }],
  steps: ["Mix", "Fry"],
};
const doubled = scale(pancakes, 4);
console.log(doubled.ingredients);

4. Test & Prove

recipes.test.mjs
import { searchByIngredient, scale } from "./recipes.mjs";

let pass = 0, fail = 0;
function test(desc, fn) {
  try { fn(); console.log("  \u2713", desc); pass++; }
  catch (e) { console.log("  \u2717", desc, "\u2014", e.message); fail++; }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }

const pancakes = {
  name: "Pancakes", servings: 2,
  ingredients: [{ name: "flour", amount: 100 }, { name: "milk", amount: 200 }],
  steps: ["Mix", "Fry"],
};
const omelette = {
  name: "Omelette", servings: 1,
  ingredients: [{ name: "eggs", amount: 3 }, { name: "milk", amount: 30 }],
  steps: ["Whisk", "Cook"],
};

test("scaling doubles every amount", () => {
  const d = scale(pancakes, 4);
  assert(d.ingredients[0].amount === 200 && d.ingredients[1].amount === 400);
});
test("scaling down halves amounts", () => {
  const h = scale(pancakes, 1);
  assert(h.ingredients[0].amount === 50);
});
test("search finds recipes by ingredient", () => {
  const found = searchByIngredient([pancakes, omelette], "milk");
  assert(found.length === 2);
});
test("search is case-insensitive and partial", () => {
  const found = searchByIngredient([pancakes, omelette], "EGG");
  assert(found.length === 1 && found[0].name === "Omelette");
});
test("scaling preserves steps", () => {
  assert(JSON.stringify(scale(pancakes, 8).steps) === JSON.stringify(["Mix", "Fry"]));
});

console.log(`\n${pass} passed, ${fail} failed`);

5. The Interface

node recipes.mjs

[
  { name: 'flour', amount: 200 },
  { name: 'milk', amount: 400 }
]

6. Run It

node recipes.mjs
node recipes.test.mjs

๐ŸŽฏ Try this next

  1. Persist recipes to a JSON file and load them on start
  2. Search by multiple ingredients at once (must contain all of them)
  3. Add a shopping list that merges ingredients across several recipes
  4. Support unit conversion (grams โ†” cups) when scaling

What you practised

Working with nested arrays of objects, case-insensitive searching with some() + includes(), proportional scaling with map(), and rounding to avoid floating-point noise.

Reference: Arrays ยท Objects ยท Functions

Try It Yourself