DEV Community

Cover image for 👨‍🍳 Your Gateway to Building CookPal: Mastering Recipe Discovery with TheMealDB
Fonyuy Gita
Fonyuy Gita

Posted on

👨‍🍳 Your Gateway to Building CookPal: Mastering Recipe Discovery with TheMealDB

Transform from cooking novice to culinary app wizard in one comprehensive guide

Table of Contents

  1. Understanding What We're Building
  2. Setting Up Your React Environment
  3. Understanding Recipe APIs
  4. Searching for Recipes
  5. Fetching Detailed Recipe Information
  6. Browsing by Categories and Ingredients
  7. Handling Recipe Data Like a Pro
  8. Error Handling and User Experience
  9. Local Development Tips for Global Access
  10. Taking It Further: Auth, AI, and Design

Understanding What We're Building

Imagine creating CookPal - the ultimate culinary companion that turns anyone into a confident cook. Your mission is to build an app that helps users discover amazing recipes, learn about ingredients, and get personalized meal suggestions based on what they have in their kitchen.

TheMealDB is your digital cookbook - containing thousands of recipes from around the world, complete with ingredients, instructions, and beautiful food photography.

Setting Up Your React Environment

Let's prepare your digital kitchen:

npx create-react-app cookpal
cd cookpal
npm start
Enter fullscreen mode Exit fullscreen mode

Think of this as organizing your kitchen before cooking a feast. React is your cooking station, and TheMealDB is your comprehensive recipe collection that never runs out of inspiration.

Understanding Recipe APIs

A recipe API is like having a master chef who knows every dish from every cuisine in the world. You can ask specific questions like "What can I make with chicken?" or "Show me Italian desserts," and they'll instantly provide detailed recipes with instructions.

TheMealDB offers several "cooking conversations":

  • Search by name: https://www.themealdb.com/api/json/v1/1/search.php?s=
  • Random meal: https://www.themealdb.com/api/json/v1/1/random.php
  • Lookup by ID: https://www.themealdb.com/api/json/v1/1/lookup.php?i=
  • Filter by ingredient: https://www.themealdb.com/api/json/v1/1/filter.php?i=
  • Browse categories: https://www.themealdb.com/api/json/v1/1/categories.php

Searching for Recipes

Let's start by creating a recipe search that feels as natural as asking a friend for cooking advice:

import React, { useState } from 'react';

function RecipeSearch() {
  const [recipes, setRecipes] = useState([]);
  const [searchTerm, setSearchTerm] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const searchRecipes = async () => {
    if (!searchTerm.trim()) {
      setError('Please enter a recipe name to search');
      return;
    }

    setLoading(true);
    setError(null);

    try {
      // Ask TheMealDB: "Do you have any recipes with this name?"
      const response = await fetch(
        `https://www.themealdb.com/api/json/v1/1/search.php?s=${encodeURIComponent(searchTerm)}`
      );

      if (!response.ok) {
        throw new Error(`Search failed: ${response.status}`);
      }

      const data = await response.json();

      // TheMealDB returns null if no recipes found
      if (data.meals) {
        setRecipes(data.meals);
      } else {
        setRecipes([]);
        setError('No recipes found. Try a different search term!');
      }

    } catch (error) {
      console.error('Recipe search failed:', error);
      setError('Failed to search recipes. Please try again.');
    } finally {
      setLoading(false);
    }
  };

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      searchRecipes();
    }
  };

  return (
    <div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
      <h2>🔍 Recipe Search</h2>

      <div style={{ marginBottom: '20px', display: 'flex', gap: '10px' }}>
        <input
          type="text"
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          onKeyPress={handleKeyPress}
          placeholder="Search for recipes (e.g., pasta, chicken, cake)..."
          style={{
            flex: 1,
            padding: '12px',
            border: '2px solid #ddd',
            borderRadius: '8px',
            fontSize: '16px'
          }}
        />
        <button
          onClick={searchRecipes}
          disabled={loading}
          style={{
            backgroundColor: '#ff6b35',
            color: 'white',
            padding: '12px 24px',
            border: 'none',
            borderRadius: '8px',
            cursor: loading ? 'not-allowed' : 'pointer',
            fontSize: '16px'
          }}
        >
          {loading ? '🔍 Searching...' : '👨‍🍳 Find Recipes'}
        </button>
      </div>

      {error && (
        <div style={{
          backgroundColor: '#ffebee',
          color: '#c62828',
          padding: '15px',
          borderRadius: '8px',
          marginBottom: '20px'
        }}>
          {error}
        </div>
      )}

      {recipes.length > 0 && (
        <div>
          <h3>🍽️ Found {recipes.length} delicious recipes:</h3>
          <div style={{ 
            display: 'grid', 
            gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', 
            gap: '20px',
            marginTop: '20px'
          }}>
            {recipes.map((recipe) => (
              <RecipeCard key={recipe.idMeal} recipe={recipe} />
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// Component to display individual recipe cards
function RecipeCard({ recipe }) {
  return (
    <div style={{
      backgroundColor: 'white',
      borderRadius: '12px',
      overflow: 'hidden',
      boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
      transition: 'transform 0.2s'
    }}>
      <img
        src={recipe.strMealThumb}
        alt={recipe.strMeal}
        style={{
          width: '100%',
          height: '200px',
          objectFit: 'cover'
        }}
      />
      <div style={{ padding: '15px' }}>
        <h3 style={{ margin: '0 0 10px 0', color: '#333' }}>
          {recipe.strMeal}
        </h3>
        <p style={{ color: '#666', fontSize: '14px', margin: '5px 0' }}>
          🌍 {recipe.strArea}  🏷️ {recipe.strCategory}
        </p>
        <button style={{
          backgroundColor: '#4CAF50',
          color: 'white',
          border: 'none',
          padding: '8px 16px',
          borderRadius: '6px',
          cursor: 'pointer',
          marginTop: '10px'
        }}>
          📖 View Recipe
        </button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Breaking this down:

  • useState keeps track of our recipe collection and search status
  • fetch() is our messenger that goes to TheMealDB's kitchen
  • encodeURIComponent() ensures search terms are properly formatted
  • The grid layout displays recipes like a beautiful cookbook spread

Fetching Detailed Recipe Information

When users want the full recipe, we need to get all the cooking details:

function RecipeDetails({ recipeId }) {
  const [recipe, setRecipe] = useState(null);
  const [loading, setLoading] = useState(false);
  const [ingredients, setIngredients] = useState([]);

  const fetchRecipeDetails = async () => {
    setLoading(true);

    try {
      const response = await fetch(
        `https://www.themealdb.com/api/json/v1/1/lookup.php?i=${recipeId}`
      );

      const data = await response.json();

      if (data.meals && data.meals[0]) {
        const recipeData = data.meals[0];
        setRecipe(recipeData);

        // Extract ingredients and measurements (TheMealDB stores them separately)
        const ingredientsList = extractIngredients(recipeData);
        setIngredients(ingredientsList);
      }

    } catch (error) {
      console.error('Failed to fetch recipe details:', error);
    } finally {
      setLoading(false);
    }
  };

  // TheMealDB stores ingredients as strIngredient1, strIngredient2, etc.
  const extractIngredients = (recipeData) => {
    const ingredients = [];

    for (let i = 1; i <= 20; i++) {
      const ingredient = recipeData[`strIngredient${i}`];
      const measure = recipeData[`strMeasure${i}`];

      if (ingredient && ingredient.trim()) {
        ingredients.push({
          name: ingredient.trim(),
          measure: measure ? measure.trim() : ''
        });
      }
    }

    return ingredients;
  };

  useEffect(() => {
    if (recipeId) {
      fetchRecipeDetails();
    }
  }, [recipeId]);

  if (loading) {
    return <div style={{ textAlign: 'center', padding: '40px' }}>
      🍳 Loading recipe details...
    </div>;
  }

  if (!recipe) {
    return <div>Recipe not found</div>;
  }

  return (
    <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
      <div style={{ textAlign: 'center', marginBottom: '30px' }}>
        <img
          src={recipe.strMealThumb}
          alt={recipe.strMeal}
          style={{
            width: '100%',
            maxWidth: '400px',
            height: '300px',
            objectFit: 'cover',
            borderRadius: '12px'
          }}
        />
        <h1 style={{ margin: '20px 0 10px 0' }}>{recipe.strMeal}</h1>
        <p style={{ color: '#666' }}>
          🌍 {recipe.strArea} Cuisine  🏷️ {recipe.strCategory}
        </p>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: '30px' }}>
        <div>
          <h2>🛒 Ingredients</h2>
          <ul style={{ listStyle: 'none', padding: 0 }}>
            {ingredients.map((ingredient, index) => (
              <li key={index} style={{
                backgroundColor: '#f8f9fa',
                padding: '10px',
                margin: '5px 0',
                borderRadius: '6px',
                border: '1px solid #e9ecef'
              }}>
                <strong>{ingredient.measure}</strong> {ingredient.name}
              </li>
            ))}
          </ul>
        </div>

        <div>
          <h2>👩‍🍳 Instructions</h2>
          <div style={{
            backgroundColor: '#fff8e1',
            padding: '20px',
            borderRadius: '8px',
            lineHeight: '1.6'
          }}>
            {recipe.strInstructions.split('\n').map((step, index) => (
              step.trim() && (
                <p key={index} style={{ marginBottom: '15px' }}>
                  <strong>Step {index + 1}:</strong> {step.trim()}
                </p>
              )
            ))}
          </div>
        </div>
      </div>

      {recipe.strYoutube && (
        <div style={{ marginTop: '30px', textAlign: 'center' }}>
          <h3>📺 Video Tutorial</h3>
          <a
            href={recipe.strYoutube}
            target="_blank"
            rel="noopener noreferrer"
            style={{
              display: 'inline-block',
              backgroundColor: '#ff0000',
              color: 'white',
              padding: '12px 24px',
              textDecoration: 'none',
              borderRadius: '8px'
            }}
          >
            🎥 Watch on YouTube
          </a>
        </div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Browsing by Categories and Ingredients

Let users explore recipes like browsing through different sections of a market:

function RecipeBrowser() {
  const [categories, setCategories] = useState([]);
  const [selectedCategory, setSelectedCategory] = useState(null);
  const [categoryRecipes, setCategoryRecipes] = useState([]);
  const [availableIngredients] = useState([
    'chicken', 'beef', 'pork', 'fish', 'rice', 'pasta', 
    'tomato', 'onion', 'garlic', 'cheese', 'eggs'
  ]);

  const fetchCategories = async () => {
    try {
      const response = await fetch('https://www.themealdb.com/api/json/v1/1/categories.php');
      const data = await response.json();
      setCategories(data.categories || []);
    } catch (error) {
      console.error('Failed to fetch categories:', error);
    }
  };

  const fetchRecipesByCategory = async (categoryName) => {
    try {
      const response = await fetch(
        `https://www.themealdb.com/api/json/v1/1/filter.php?c=${categoryName}`
      );
      const data = await response.json();
      setCategoryRecipes(data.meals || []);
      setSelectedCategory(categoryName);
    } catch (error) {
      console.error('Failed to fetch recipes by category:', error);
    }
  };

  const fetchRecipesByIngredient = async (ingredient) => {
    try {
      const response = await fetch(
        `https://www.themealdb.com/api/json/v1/1/filter.php?i=${ingredient}`
      );
      const data = await response.json();
      setCategoryRecipes(data.meals || []);
      setSelectedCategory(`Recipes with ${ingredient}`);
    } catch (error) {
      console.error('Failed to fetch recipes by ingredient:', error);
    }
  };

  const getRandomRecipe = async () => {
    try {
      const response = await fetch('https://www.themealdb.com/api/json/v1/1/random.php');
      const data = await response.json();
      if (data.meals && data.meals[0]) {
        setCategoryRecipes([data.meals[0]]);
        setSelectedCategory('Random Recipe Surprise');
      }
    } catch (error) {
      console.error('Failed to fetch random recipe:', error);
    }
  };

  useEffect(() => {
    fetchCategories();
  }, []);

  return (
    <div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
      <h2>🗂️ Browse Recipes</h2>

      <div style={{ marginBottom: '30px' }}>
        <button
          onClick={getRandomRecipe}
          style={{
            backgroundColor: '#9c27b0',
            color: 'white',
            padding: '12px 24px',
            border: 'none',
            borderRadius: '8px',
            cursor: 'pointer',
            marginBottom: '20px'
          }}
        >
          🎲 Surprise Me!
        </button>
      </div>

      <div style={{ marginBottom: '30px' }}>
        <h3>🏷️ Browse by Category</h3>
        <div style={{ 
          display: 'grid', 
          gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', 
          gap: '15px' 
        }}>
          {categories.slice(0, 8).map((category) => (
            <button
              key={category.idCategory}
              onClick={() => fetchRecipesByCategory(category.strCategory)}
              style={{
                backgroundColor: 'white',
                border: '2px solid #ddd',
                borderRadius: '8px',
                padding: '15px',
                cursor: 'pointer',
                textAlign: 'center',
                transition: 'all 0.2s'
              }}
            >
              <div style={{ fontSize: '24px', marginBottom: '8px' }}>
                🍽️
              </div>
              <div style={{ fontWeight: 'bold' }}>
                {category.strCategory}
              </div>
            </button>
          ))}
        </div>
      </div>

      <div style={{ marginBottom: '30px' }}>
        <h3>🥘 Browse by Ingredient</h3>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
          {availableIngredients.map((ingredient) => (
            <button
              key={ingredient}
              onClick={() => fetchRecipesByIngredient(ingredient)}
              style={{
                backgroundColor: '#e8f5e8',
                border: '1px solid #4caf50',
                borderRadius: '20px',
                padding: '8px 16px',
                cursor: 'pointer'
              }}
            >
              {ingredient}
            </button>
          ))}
        </div>
      </div>

      {selectedCategory && categoryRecipes.length > 0 && (
        <div>
          <h3>🍴 {selectedCategory} ({categoryRecipes.length} recipes)</h3>
          <div style={{ 
            display: 'grid', 
            gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', 
            gap: '20px' 
          }}>
            {categoryRecipes.map((recipe) => (
              <RecipeCard key={recipe.idMeal} recipe={recipe} />
            ))}
          </div>
        </div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Error Handling and User Experience

Cooking apps should be forgiving - like a patient cooking instructor:

const [networkError, setNetworkError] = useState(false);
const [retryAttempts, setRetryAttempts] = useState(0);

const fetchWithRetry = async (url, maxRetries = 3) => {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url);

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      setNetworkError(false);
      setRetryAttempts(0);
      return await response.json();

    } catch (error) {
      if (attempt === maxRetries) {
        setNetworkError(true);
        setRetryAttempts(attempt + 1);
        throw error;
      }

      // Wait before retrying (exponential backoff)
      await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
    }
  }
};

// Graceful handling of missing recipe data
const formatRecipeData = (recipe) => {
  return {
    id: recipe.idMeal,
    name: recipe.strMeal || 'Unnamed Recipe',
    image: recipe.strMealThumb || '/placeholder-food.jpg',
    category: recipe.strCategory || 'Unknown',
    area: recipe.strArea || 'International',
    instructions: recipe.strInstructions || 'Instructions not available',
    youtube: recipe.strYoutube || null,
    ingredients: extractIngredients(recipe)
  };
};

// Show helpful error messages
const ErrorMessage = ({ error, onRetry }) => (
  <div style={{
    backgroundColor: '#ffebee',
    border: '1px solid #f44336',
    borderRadius: '8px',
    padding: '20px',
    textAlign: 'center',
    margin: '20px 0'
  }}>
    <h3>🤔 Oops! Something went wrong</h3>
    <p>{error}</p>
    <button
      onClick={onRetry}
      style={{
        backgroundColor: '#f44336',
        color: 'white',
        border: 'none',
        padding: '10px 20px',
        borderRadius: '6px',
        cursor: 'pointer'
      }}
    >
      🔄 Try Again
    </button>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

Local Development Tips for Global Access

Working from Bamenda or areas with slower connections? Here are some optimization tips:

// Cache frequently accessed data
const recipeCache = new Map();
const CACHE_DURATION = 10 * 60 * 1000; // 10 minutes

const getCachedRecipe = (id) => {
  const cached = recipeCache.get(id);
  if (cached && (Date.now() - cached.timestamp) < CACHE_DURATION) {
    return cached.data;
  }
  return null;
};

const setCachedRecipe = (id, data) => {
  recipeCache.set(id, {
    data,
    timestamp: Date.now()
  });
};

// Optimize image loading
const LazyImage = ({ src, alt, ...props }) => {
  const [loaded, setLoaded] = useState(false);
  const [error, setError] = useState(false);

  return (
    <div style={{ position: 'relative', backgroundColor: '#f5f5f5' }}>
      {!loaded && !error && (
        <div style={{
          position: 'absolute',
          top: '50%',
          left: '50%',
          transform: 'translate(-50%, -50%)'
        }}>
          🍽️ Loading...
        </div>
      )}
      <img
        src={src}
        alt={alt}
        onLoad={() => setLoaded(true)}
        onError={() => setError(true)}
        style={{
          ...props.style,
          opacity: loaded ? 1 : 0,
          transition: 'opacity 0.3s'
        }}
      />
      {error && (
        <div style={{
          position: 'absolute',
          top: '50%',
          left: '50%',
          transform: 'translate(-50%, -50%)',
          textAlign: 'center'
        }}>
          🖼️ Image unavailable
        </div>
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Taking It Further: Auth, AI, and Design

Congratulations! You now have the foundation to build an amazing recipe discovery app. But your CookPal journey has just begun. Here's where you can take it next:

🔐 Authentication with Firebase

Consider adding user accounts so people can:

  • Save favorite recipes to personal cookbooks
  • Create custom meal plans and shopping lists
  • Rate and review recipes they've tried
  • Share their own recipe modifications

Getting Started: Visit Firebase Console and explore Authentication services. Food apps become much more personal with user accounts!

🤖 Adding AI Magic

Imagine enhancing your app with intelligent features:

  • Smart Meal Planning: "Plan my week based on these dietary preferences"
  • Ingredient Substitutions: "What can I use instead of buttermilk?"
  • Dietary Analysis: Automatically identify vegan, gluten-free, or keto recipes
  • Cooking Assistant: Real-time help while following recipes

Getting Started: Explore Google's Gemini API or OpenAI's services to add conversational cooking assistance.

🎨 Design Inspiration

Your cooking app should be as appetizing as the recipes it showcases:

  • Dribbble: Search for "recipe app UI" or "food discovery interface"
  • Behance: Browse "cooking app design" or "food photography layouts"
  • Material Design: Google's design system for content-rich applications
  • Food Photography: Study how food delivery apps present meals visually

💡 Feature Ideas to Explore

  • Smart Shopping Lists: Generate ingredient lists from selected recipes
  • Cooking Timers: Built-in timers for each recipe step
  • Nutritional Information: Calculate calories and nutrients
  • Social Features: Share cooking successes with friends
  • Recipe Scaling: Adjust ingredient quantities for different serving sizes
  • Offline Mode: Save recipes for cooking without internet

🌍 Local Considerations

For users in Cameroon and similar regions:

  • Local Ingredients: Suggest African ingredient substitutions
  • Data Efficiency: Optimize images and cache popular recipes
  • Multi-language Support: Interface in French and English
  • Regional Cuisines: Highlight West African and international fusion recipes

Your Mission Awaits

You now hold the keys to unlock thousands of delicious recipes from around the world. Your CookPal app could become the cooking companion that transforms kitchens across Africa and beyond, helping people discover new flavors and cooking techniques.

Remember: every great chef started with their first recipe. You've just learned how to access thousands of them digitally. The culinary adventures you'll enable are endless.

Ready to cook up something amazing? Your hungry users are waiting for the perfect recipe discovery experience. Make it happen! 🚀


Happy coding, future culinary tech pioneer!

Top comments (1)

Collapse
 
fonyuygita profile image
Fonyuy Gita

You now hold the keys to unlock thousands of delicious recipes from around the world. Your CookPal app could become the cooking companion that transforms kitchens across Africa and beyond, helping people discover new flavors and cooking techniques.