犬とレシピで学ぶ!API叩きアプリ2連発

Published
2025-10-12
Author
MT
Tags

はじめに


今回は学習の一環として、2つのアプリを実装してみました。

👉 Free Recipe API : https://free-recipe-api.vercel.app/


どうも、エンジニア歴四ヶ月の者です。


前回の Todo アプリ では、基本的な機能を一通り実装しました。

タスクを追加したり削除したり、チェックボックスをオンにすると完了したタスクに取り消し線が入るようにして、ちょっとした見た目の変化も加えたり。


さらに、コンポーネント間での値を渡す練習として props や $emit を使い、Vue の基本的な仕組みにも触れることができました。


そうした基礎に少しずつ慣れてきたこともあり、今回は API 通信を使ったアプリ を作ってみることにしました。


これまで API を叩くといえば Udemy の講座を見ながら手を動かす程度で、自分でゼロから実装してみるのは初めてだったかもしれません…。

React では JSON 形式のデータを描画するアプリを何度か作ったことはありましたが、そのときは生成 AI にかなり頼っていたので、今振り返ると「自分で説明できないアプリは本当の意味で自作とは言えないのでは」と感じています…


ということで、本題です!


Dog API & Free Recipe API

今回は Dog API と Free Recipe API を使って、実際に API を叩き、そのデータをどう描画するかに挑戦しました。


Dog API ドキュメント

シンプルな API で、実装もわかりやすかったです。初めて API 通信を学ぶ題材としてはおすすめだと感じました。エンドポイントを叩くだけで犬の画像が返ってくるので、動いている実感が得やすかったです。


Free Recipe API ドキュメント

今回は v1/1/random.php を叩いてランダムなレシピを取得しましたが、ドキュメントを見ると検索やカテゴリ別フィルタなど、他にもさまざまなエンドポイントが用意されているようでした。


今回は Dog API と同じように「ランダムなデータを返すエンドポイント」を使ったのですが、Dog API のようにエンドポイントを叩けばすぐ描画できるわけではなく、少し試行錯誤が必要でした


コンソールを見るとレスポンスが返ってきているのに、実際にその配列の 0 番目の要素をどう扱えばいいのか、どんな形式の変数に入れて、どのように描画すればよいのかが最初は分からず迷ってしまいました。


ということで、実装の流れや気づきをざっくり書いていけたらと思います。


1.Dog API

実際に作ったアプリ↓


冒頭にもリンクを貼りましたが、こちらが Dog API を使ったアプリ になります。


ボタンをクリックするとランダムで犬の画像が表示されるシンプルな作りですが、API 通信 → レスポンス受け取り → 描画 という一連の流れを体験できる、ちょうどいい題材でした。


App.vue

<template>
  <div id="app">
    <div class="container">
      <h1 class="title">Random Dog</h1>

      <button class="btn" :disabled="isLoading" @click="randomDogImg"
      >
        <span v-if="!isLoading">Next Dog</span>
        <span v-else class="spinner"></span>
      </button>

      <div class="card">
        <transition name="fade">
          <img v-if="dogImg" :src="dogImg" class="dog-img"
          />
        </transition>
        <div v-if="isLoading && !dogImg" class="skeleton"></div>
      </div>
      <p class="hint">Powered by dog.ceo</p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      dogImg: '',
      isLoading: false
    }
  },
  methods: {
    async randomDogImg() {
      if (this.isLoading) return;
      this.isLoading = true;
      try {
        const url = "https://dog.ceo/api/breeds/image/random";
        const res = await fetch(url);
        const data = await res.json();
        this.dogImg = data.message;
        await new Promise(r => setTimeout(r, 200));
      } catch (e) {
        console.error(e);
      }
      this.isLoading = false;
    }
  },
  mounted() {
    this.randomDogImg();
  }
}
</script>


API を取得する方法が実務的にベストなのかどうかはまだ分かりませんが、個人的には .then をつないで書くよりも 上記のように書いた方が書きやすいと感じています。そのため、今回はこのスタイルで実装しました。


また、現場のコードを見ていると try/catch がよく使われているので、今回はそのパターンも意識して取り入れてみました。


また setTimeout 関数を入れている理由は、ボタンの状態が「Loading...」から「Next Dog」に戻るタイミングと、画像が実際に描画されるタイミングにズレがあったからです。

await new Promise(r => setTimeout(r, 200));

API のレスポンス自体はすぐに返ってきてボタンは正常に戻るのですが、その時点ではまだ画像が画面に描画されていない瞬間がありました。


そこで処理をほんの少し遅らせることで、ボタンが戻るタイミングと画像が表示されるタイミングの違和感が少し改善できたと思っています。


2.Free Recipe API

実際に作ったアプリ↓


冒頭にもリンクを貼りましたが、こちらがFree Recipe API を使ったアプリ になります。


App.vue

<template>
  <div id="app">
    <div v-if="item" class="meal-card">
      <h2 class="meal-title">{{ item.title }}</h2>
      <p class="meal-meta">
        <span>{{ item.area }}</span><span>{{ item.category }}</span>
      </p>
      <div class="meal-flex">
        <img class="meal-img" :src="item.img" />
        <div class="meal-instructions">
          <div class="meal-text">
            <p>{{ item.instructions }}</p>
          </div>
          <div class="meal-flex-button">
            <button class="btn" :disabled="isLoading" @click="fetchRandomMeal">
              <span>{{ isLoading ? "Loading..." : "Next Meal" }}</span>
           </button>
          </div>
        </div>
      </div>

    </div>


  </div>
</template>

<script>
const sleep = () => new Promise((resolve) => setTimeout(resolve, 100));
export default {
  data() {
    return {
      url: 'https://www.themealdb.com/api/json/v1/1/random.php',
      item: null,
      isLoading: false
    }
  },
  methods: {
    async fetchRandomMeal() {
      if(this.isLoading) {
        return
      }
      this.isLoading = true
      try {
        const res = await fetch(this.url)
        const data = await res.json()
        const meal = data.meals[0]
        console.log(meal);

        this.item = {
          id: meal.idMeal,
          title: meal.strMeal,
          area: meal.strArea,
          category: meal.strCategory,
          instructions: meal.strInstructions,
          img: meal.strMealThumb,
        }
      } catch(error) {
        console.log(error);
      }
      await sleep();
      this.isLoading = false
    }
  },
  mounted() {
    this.fetchRandomMeal()
  }
}
</script>


仕様自体は Dog API に近いのですが、Free Recipe API では必ず meals という配列の 0 番目に、レシピの詳細が オブジェクト形式で入って返ってきていて、コンソールで中身を確認するところまではすぐにできたものの、実際に どのプロパティを取り出して、どんな形に整えて描画するかで少し試行錯誤しました。


最終的には、data.meals[0] を meal という変数に格納し、そこから欲しいプロパティだけを取り出して item に整形する形にしました。

        this.item = {
          id: meal.idMeal,
          title: meal.strMeal,
          area: meal.strArea,
          category: meal.strCategory,
          instructions: meal.strInstructions,
          img: meal.strMealThumb,
        }


実装してみて気づいたポイント


1. mounted()

この二つのアプリを作っていくなかで、Todo アプリとの違いがありました。Todo ではユーザーが入力してはじめて処理が動きますが、Dog API や Free Recipe API のように「ページを開いた瞬間にデータを表示したい」ケースもあります。


そのとき便利なのが mounted() !


コンポーネントがマウントされたタイミングで処理を自動的に実行できるので、ユーザーが何かを入力しなくても最初から API を叩いて結果を表示することができました。


改めて振り返ると、コードレビューで「ここは mounted() に書き直した方がいい」とレビューいただいた経験がありました。


ただ、実務ではコード量が膨大なことも多く、その場では理解が浅くなりがちで、「なぜそうするのか」を深く考える余裕が持てていなかった ように思います。


今回の実装を通して、あのときのコードレビューの意図がよりクリアに理解できたと感じています。

  mounted() {
    this.fetchRandomMeal()
  }


2. ボタンの制御について

また実装していく中で、ボタンの制御も学びました。

if (this.isLoading) {
  return
}
this.isLoading = true
try {
  // API 通信処理 ...
} catch (error) {
  console.log(error)
}
this.isLoading = false

実行されると、まず this.isLoading = true にして :disabled="isLoading" が true になり、ボタンはクリックできない状態 になります。


その次に API の処理が実行され、完了すると this.isLoading = false に戻す ことで、最初の状態に復帰します。


また、もしユーザーが連続でボタンをクリックしてしまっても、

if (this.isLoading) {
  return
}

と書いておけば、処理中はその場で中断して二重リクエストを防止できます。

これは 実務でも何度も目にするクリック制御 だなあと感じながら、改めて自分でも実装してみました。


3.命名規則について

ブーリアン型の変数名には is や has を付けるのが良い という記事を読んだので、実際に今回のアプリで取り入れてみました。

isLoading: false

前回の Todo アプリでは完了状態を表す変数を単に complete と書いてしまっていましたが、今回のアプリではローディング状態を loading ではなく isLoading にしています。


振り返ってみると、実際に現場のコードでも isActive や hasError のように is や has を付けた命名が多く使われていました。


こうすることで、変数がブーリアンであることが一目で分かり、コードの可読性が上がるんだなと実感しました。今後もこうしたルールを意識して身につけていきたいと思います。


まとめ

今回、Dog API と Free Recipe API を使って、API 通信の基本を一通り体験することができました。


これまで Udemy や生成 AI に頼ってコードを書いていた部分も多かったのですが、今回のように自分でゼロから実装してみると「なぜそう書くのか」「どのプロパティを使うのか」といった理解がぐっと深まることを実感しました。


また、意外と CSS の調整に時間がかかることや、思わぬところで沼りやすいこと も学びのひとつでした。完璧を目指すと先に進めなくなってしまうので、今回は 「ほどほど」を目標にしながらアプリを作る ことを意識しました。


それでは

最後まで読んでいただき、ありがとうございました!!