Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Miguelcds/Recipe-Hub/llms.txt

Use this file to discover all available pages before exploring further.

All network requests to TheMealDB are centralized in a single file: src/services/api.js. No component or hook fetches data directly — they all call one of the exported functions from this module. This keeps network logic out of the UI layer and makes it straightforward to test, mock, or swap the data source later.

Full source

src/services/api.js
const URL_BASE = "https://themealdb.com/api/json/v1/1/";

const apiOk = (res) => {
  if (!res.ok) {
    throw new Error(`The Meal Error: ${res.status}`);
  }
};

const apiMealOk = (data) => {
  if (!data.meals?.length) {
    throw new Error(
      "No se ha localizado ninguna receta con los datos introducidos",
    );
  }
};

// Consulta API para Busqueda por Nombre
export const getRecipeByName = async (search) => {
  const res = await fetch(`${URL_BASE}search.php?s=${search}`);
  apiOk(res);
  const data = await res.json();
  apiMealOk(data);
  return data.meals.map((e) => {
    return {
      id: e.idMeal,
      name: e.strMeal,
      picture: e.strMealThumb,
      category: e.strCategory,
    };
  });
};

// Consulta API para Busqueda por ID
export const getRecipeById = async (id) => {
  const res = await fetch(`${URL_BASE}lookup.php?i=${id}`);
  apiOk(res);
  const data = await res.json();
  apiMealOk(data);

  const meal = data.meals[0];
  const ingredients = [];

  for (let i = 1; i <= 20; i++) {
    const ingredient = meal[`strIngredient${i}`];
    const measure = meal[`strMeasure${i}`];
    if (ingredient && ingredient.trim() !== "") {
      ingredients.push({
        name: ingredient,
        measure: measure,
      });
    }
  }

  return {
    id: meal.idMeal,
    name: meal.strMeal,
    picture: meal.strMealThumb,
    category: meal.strCategory,
    instructions: meal.strInstructions,
    video: meal.strYoutube,
    ingredients,
  };
};

export const getRandomRecipes = async () => {
  const requests = Array.from({ length: 9 }, () =>
    fetch(`${URL_BASE}random.php`),
  );
  const responses = await Promise.all(requests);
  responses.forEach(apiOk);
  const data = await Promise.all(responses.map((res) => res.json()));
  data.forEach(apiMealOk);
  const meals = data.map((d) => d.meals[0]);
  return meals.map((e) => ({
    id: e.idMeal,
    name: e.strMeal,
    picture: e.strMealThumb,
    category: e.strCategory,
  }));
};

Private helpers

Two private functions validate every response before any data is processed. They are not exported and cannot be called outside this module.
Checks the HTTP response status. Called immediately after every fetch call.
const apiOk = (res) => {
  if (!res.ok) {
    throw new Error(`The Meal Error: ${res.status}`);
  }
};
Throws: Error with message "The Meal Error: {status}" when res.ok is false (i.e., any non-2xx HTTP status code).
Checks the parsed JSON payload. Called after res.json() to confirm that the API returned at least one meal.
const apiMealOk = (data) => {
  if (!data.meals?.length) {
    throw new Error(
      "No se ha localizado ninguna receta con los datos introducidos",
    );
  }
};
Throws: Error with message "No se ha localizado ninguna receta con los datos introducidos" when data.meals is null, undefined, or an empty array. TheMealDB returns { "meals": null } for queries that match nothing.

Exported functions

Searches TheMealDB by meal name and returns a list of matching meals. Parameters
The meal name to search for. Passed directly to the s query parameter of the TheMealDB search endpoint.
Returns Promise<Array<{id: string, name: string, picture: string, category: string}>>
id
string
required
TheMealDB meal ID (idMeal).
name
string
required
Meal name (strMeal).
picture
string
required
URL to the meal thumbnail (strMealThumb).
category
string
required
Meal category (strCategory).
Throws
  • apiOk throws if the HTTP request fails (non-2xx status).
  • apiMealOk throws if no meals match the search query.

getRecipeById(id)

Fetches the full detail record for a single meal by its TheMealDB ID. Parameters
id
string
required
The TheMealDB meal ID. Typically obtained from a getRecipeByName or getRandomRecipes result.
Returns Promise<{id, name, picture, category, instructions, video, ingredients}>
id
string
required
TheMealDB meal ID.
name
string
required
Meal name.
picture
string
required
Thumbnail URL.
category
string
required
Meal category.
instructions
string
required
Full cooking instructions (strInstructions).
video
string
YouTube URL for the recipe (strYoutube). May be an empty string.
ingredients
object[]
required
Parsed list of ingredients. See below for the item shape.
Ingredient parsing TheMealDB encodes ingredients as 20 flat fields (strIngredient1strIngredient20) rather than an array. getRecipeById normalizes this into a clean array:
1

Iterate over all 20 slots

A for loop runs from i = 1 to i = 20, reading meal[\strIngredienti]ˋandmeal[sˋtrMeasure{i}\`]` and `meal[\`strMeasure`]`.
2

Filter empty slots

If the ingredient string is falsy or whitespace-only (ingredient.trim() === ""), the slot is skipped. TheMealDB fills unused slots with "" or null.
3

Push to the array

Valid { name, measure } pairs are pushed to the ingredients array, which is returned as part of the meal object.
Throws
  • apiOk throws if the HTTP request fails.
  • apiMealOk throws if the ID does not exist in TheMealDB.

getRandomRecipes()

Fetches 9 random meals in parallel and returns them as an array. This function takes no parameters. Returns Promise<Array<{id: string, name: string, picture: string, category: string}>> — always resolves to exactly 9 items when successful. Parallel fetch strategy The function uses Array.from with a mapping function to create 9 fetch Promise objects simultaneously, then settles them all with Promise.all:
parallel fetch
// Create 9 in-flight fetch promises at once
const requests = Array.from({ length: 9 }, () =>
  fetch(`${URL_BASE}random.php`),
);

// Wait for all responses, then parse all JSON bodies
const responses = await Promise.all(requests);
responses.forEach(apiOk);
const data = await Promise.all(responses.map((res) => res.json()));
This is significantly faster than awaiting each request sequentially, since all 9 network round-trips happen concurrently. Throws
  • apiOk throws if any of the 9 HTTP responses is not OK. Because forEach(apiOk) runs synchronously after Promise.all, the first failing response causes an immediate throw.
  • apiMealOk throws if any response body contains no meals.

Error handling

Errors propagate through a consistent chain from the service layer to the UI:
src/services/api.js
// apiOk throws on non-2xx HTTP status
if (!res.ok) {
  throw new Error(`The Meal Error: ${res.status}`);
}

// apiMealOk throws when the API returns no results
if (!data.meals?.length) {
  throw new Error("No se ha localizado ninguna receta con los datos introducidos");
}
Because all three service functions throw Error objects with descriptive messages, the hook does not need to distinguish between error types — it always sets apiError to error.message and the page renders it directly.

Extending the service layer

To add a new endpoint — for example, filtering meals by category (filter.php?c=Seafood) — follow the same three-step pattern used by every existing function: fetch the URL, validate the HTTP response with apiOk, validate the data with apiMealOk, then map the raw meal objects to a clean shape before returning them.
adding a new endpoint
export const getRecipesByCategory = async (category) => {
  const res = await fetch(`${URL_BASE}filter.php?c=${category}`);
  apiOk(res);
  const data = await res.json();
  apiMealOk(data);
  return data.meals.map((e) => ({
    id: e.idMeal,
    name: e.strMeal,
    picture: e.strMealThumb,
  }));
};