DEV Community

Doushabao
Doushabao

Posted on

I Built a Zero-Cost Recipe-to-World Tool With 3 Free APIs — Here's the Full Walkthrough

Last week I wanted to build something small but useful. A command-line tool that takes a recipe name, finds the full recipe with ingredients and instructions, translates it to any language, and converts the listed prices to my local currency.

Three free APIs. No API keys. No credit card. The whole thing runs in about 2 seconds.

Here's exactly how I built it, what broke, and what I'd do differently.

The Three APIs

I picked three services that are completely free and require zero authentication:

  1. TheMealDB — a recipe database with 300+ meals, ingredients, and instructions
  2. LibreTranslate — open-source translation (hosted instances available, or self-host)
  3. ExchangeRate-API — free currency conversion rates

The idea: chain them together so you can type recipe "chicken curry" and get the full recipe in Japanese, with ingredient prices converted to Yen.

Step 1: Fetching a Recipe

TheMealDB is refreshingly simple. You search by name and get back a JSON object with the meal name, category, instructions, and a list of up to 20 ingredients with measurements.

In JavaScript, it's just a fetch call:

async function getRecipe(name) {
  const response = await fetch(
    `https://www.themealdb.com/api/json/v1/1/search.php?s=${encodeURIComponent(name)}`
  );
  const data = await response.json();

  if (!data.meals || data.meals.length === 0) {
    throw new Error(`No recipe found for "${name}"`);
  }

  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.trim(), amount: measure?.trim() || '' });
    }
  }

  return {
    name: meal.strMeal,
    category: meal.strCategory,
    area: meal.strArea,
    instructions: meal.strInstructions,
    ingredients
  };
}
Enter fullscreen mode Exit fullscreen mode

Pitfall #1: TheMealDB uses numbered keys (strIngredient1, strIngredient2, ...) instead of an array. This is common with older APIs — always inspect the full response structure before assuming a clean format.

Step 2: Translating the Recipe

For translation, I used LibreTranslate. There are several free hosted instances, or you can run your own.

The API is straightforward — send text and a target language code:

async function translateText(text, targetLang) {
  // Split long text into chunks (LibreTranslate has character limits)
  const chunks = splitIntoChunks(text, 500);
  const translated = [];

  for (const chunk of chunks) {
    const response = await fetch('https://libretranslate.com/translate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        q: chunk,
        source: 'en',
        target: targetLang
      })
    });

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

    const result = await response.json();
    translated.push(result.translatedText);
  }

  return translated.join(' ');
}

function splitIntoChunks(text, maxLen) {
  const chunks = [];
  let remaining = text;
  while (remaining.length > 0) {
    if (remaining.length <= maxLen) {
      chunks.push(remaining);
      break;
    }
    // Try to split at a sentence boundary
    let splitPoint = remaining.lastIndexOf('. ', maxLen);
    if (splitPoint === -1 || splitPoint < maxLen * 0.5) {
      splitPoint = remaining.lastIndexOf(' ', maxLen);
    }
    if (splitPoint === -1) splitPoint = maxLen;
    chunks.push(remaining.slice(0, splitPoint + 1));
    remaining = remaining.slice(splitPoint + 1);
  }
  return chunks;
}
Enter fullscreen mode Exit fullscreen mode

Pitfall #2: Free translation APIs have character limits per request. A full recipe can be 2,000+ characters. If you don't chunk the text, you'll get 400 errors with no explanation.

Pitfall #3: Some LibreTranslate instances have rate limits of 1-5 requests per minute. If you're translating each ingredient separately, you'll hit the limit fast. Batch what you can.

Step 3: Currency Conversion

ExchangeRate-API gives you the latest rates with a simple GET request:

async function convertCurrency(amount, fromCurrency, toCurrency) {
  const response = await fetch(
    `https://open.er-api.com/v6/latest/${fromCurrency}`
  );
  const data = await response.json();

  if (data.result !== 'success') {
    throw new Error('Currency conversion failed');
  }

  const rate = data.rates[toCurrency];
  if (!rate) {
    throw new Error(`Unknown currency: ${toCurrency}`);
  }

  return {
    original: amount,
    converted: Math.round(amount * rate * 100) / 100,
    rate,
    from: fromCurrency,
    to: toCurrency
  };
}
Enter fullscreen mode Exit fullscreen mode

This one is the simplest of the three. No pagination, no chunking, just a clean JSON response.

Pitfall #4: Exchange rates update daily, not in real-time. If your tool needs live pricing (like for an e-commerce app), this API won't cut it. For recipe ingredients, it's more than enough.

Putting It All Together

Here's the main function that chains all three APIs:

async function recipeToWorld(recipeName, targetLang, targetCurrency) {
  console.log(`🔍 Searching for "${recipeName}"...`);
  const recipe = await getRecipe(recipeName);

  console.log(`🌐 Translating to ${targetLang}...`);
  const translatedName = await translateText(recipe.name, targetLang);
  const translatedInstructions = await translateText(recipe.instructions, targetLang);

  const translatedIngredients = [];
  for (const ing of recipe.ingredients) {
    const translatedName = await translateText(ing.name, targetLang);
    translatedIngredients.push({ ...ing, name: translatedName });
  }

  console.log(`💱 Converting prices to ${targetCurrency}...`);
  // Note: TheMealDB doesn't include prices, so we estimate
  // based on typical grocery costs
  const estimatedPrices = estimateIngredientCosts(recipe.ingredients);

  return {
    name: translatedName,
    category: recipe.category,
    area: recipe.area,
    instructions: translatedInstructions,
    ingredients: translatedIngredients.map((ing, i) => ({
      ...ing,
      estimatedCost: estimatedPrices[i]
    }))
  };
}
Enter fullscreen mode Exit fullscreen mode

Pitfall #5: TheMealDB doesn't include ingredient prices. I had to build a small cost estimation table for common ingredients. This is the kind of thing that's obvious in hindsight but easy to miss when you're excited about chaining APIs.

The Hard Part: Error Handling

Everything above works perfectly when the APIs are up and returning good data. In reality:

  • TheMealDB occasionally returns empty results for valid recipe names
  • LibreTranslate instances go down without warning
  • ExchangeRate-API rate limits at 1,000 requests/month on the free tier

Here's my error handling pattern:

async function safeFetch(fn, fallback, retries = 2) {
  for (let i = 0; i <= retries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === retries) {
        console.warn(`⚠️ Using fallback: ${error.message}`);
        return fallback;
      }
      // Wait before retrying (exponential backoff)
      await new Promise(r => setTimeout(r, 1000 * (i + 1)));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Use it like this:

const recipe = await safeFetch(
  () => getRecipe(name),
  { name: name, instructions: 'Recipe not found', ingredients: [] }
);

const translated = await safeFetch(
  () => translateText(recipe.instructions, lang),
  recipe.instructions  // Fall back to English
);
Enter fullscreen mode Exit fullscreen mode

Pitfall #6: Free APIs don't guarantee uptime. If you build something that chains 3+ APIs together, you need fallbacks for each one. A chain is only as strong as its weakest link.

What I'd Do Differently

  1. Cache aggressively. Exchange rates change daily. Recipe data rarely changes. Cache both locally and reduce API calls.

  2. Use a translation library as a fallback. If LibreTranslate is down, having a local fallback (even a simple dictionary for common cooking terms) keeps the tool functional.

  3. Add input validation early. Recipe names with special characters can cause URL encoding issues. Sanitize inputs before the first API call.

  4. Handle encoding properly. Translating to languages like Chinese, Japanese, or Arabic means dealing with UTF-8 carefully. Some older tools mess this up.

The Result

The final tool is about 150 lines of JavaScript. It takes three arguments:

$ node recipe-world.js "chicken curry" ja JPY

🔍 Searching for "chicken curry"...
🌐 Translating to Japanese...
💱 Converting prices to JPY...

チキンカレー (Indian)
Category: Chicken
Area: Indian

Ingredients:
- 鶏肉 (Chicken) — est. ¥350
- ココナッツミルク (Coconut Milk) — est. ¥200
- カレーパウダー (Curry Powder) — est. ¥150
...

Instructions:
チキンを一口大に切ります。...
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

Building with free APIs is genuinely fun. The biggest lesson: treat every API call as if it will fail. Rate limits, downtime, unexpected response formats — they're not edge cases, they're the normal case for free services.

If you're building a side project and need recipe data, translation, or currency conversion, all three of these APIs are solid starting points. Just remember to chunk your translation requests, cache your exchange rates, and always have a fallback plan.


If you're looking for a convenient way to access multiple free APIs through a single endpoint (translation, weather, food data, and more), I've been using a tool called QuotaLink that aggregates these services. It saves me from managing three separate API integrations.

Top comments (0)