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
// 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
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
- Persist recipes to a JSON file and load them on start
- Search by multiple ingredients at once (must contain all of them)
- Add a shopping list that merges ingredients across several recipes
- Support unit conversion (grams โ cups) when scaling