PokéAPIアプリを自作!これまでの知識と新しい挑戦

Published
2026-03-20
Author
MT
Tags

はじめに


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

早いもので、気づけばエンジニアとして半年が経とうとしています。


現場では毎日コードを見ているうちに、

少しずつ「なんとなく読める」ようになってきました。


とはいえ、今の業務は保守・運用がメイン。

自分で一から作るとなると、またまったく違う難しさがあるんだろうなとも思っています。


本題


ということで、ここからは本題です。


前回のブログでは JSONPlaceholderのAPI を利用して、

簡単な検索アプリを作りました。


今回はその応用として、検索機能を含んだ PokéAPI アプリを制作してみました!



新たに モーダル実装 にも挑戦し、

コンポーネント分割 を意識して構成してみました。


ちなみに、URLが 「seven-black」 という

とんでもなく厨二病っぽい感じになっていますが

これは Vercel の自動生成URL です…


コードの説明


①二段階フェッチ

今回いちばんハマったのは、「二段階フェッチ」 でした。

一覧エンドポイント(/pokemon?offset=&limit=)を叩けば、

そのまま v-for で描画できると思っていたのですが、

返ってくるのは各ポケモンの名前と、

その詳細データが入っているエンドポイントのURLだけ…。


つまり、

一覧を API を叩いて取得 →

その中の詳細エンドポイントをもう一度叩く

という二段構えが必要だったんです。


実装としては、まず一覧を fetch → json で取得し、

そこから results.map(...) で詳細データを Promise.all を使って並列取得。


for で回すと1件ずつ順番に処理するのに対して、

Promise.all は複数のリクエストを同時に実行できる

という仕組みを学びました。


最終的には、全てのデータが揃った段階で、

テンプレートで扱いやすい形に整形してから描画する、

という流れにしています。

    async pokemonApi(offset, limit) {
      try {
        // ここは fetch 専用(データだけ取得)
        const listUrl = `https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`
        const listRes = await fetch(listUrl)

        if (listRes.status !== 200) {
          throw new Error(listRes.status);
        }
        // 一度APIを叩いて、各ポケモンの詳細URL(エンドポイント一覧)を取得
        const listData = await listRes.json()

        // 各ポケモンの詳細データを更に叩く
        const details = await Promise.all(
          listData.results.map(async (item) => {
            const res = await fetch(item.url)

            if (res.status !== 200) {
              throw new Error(res.status);
            }

            const detail = await res.json()

            const stats = Object.fromEntries(
              detail.stats.map(s => [s.stat.name, s.base_stat])
            )
            const types = detail.types.map(t => t.type.name)

            // 取得したAPIレスポンスを、テンプレートで扱いやすいように整形
            return {
              id: detail.id,
              name: detail.name.charAt(0).toUpperCase() + detail.name.slice(1),
              image:
                detail.sprites?.other?.['official-artwork']?.front_default ??
                detail.sprites?.front_default ??
                '',
              types,
              hp:  stats.hp ?? 50,
              atk: stats.attack ?? 50,
              def: stats.defense ?? 50,
              spd: stats.speed ?? 50,
              spAtk: stats['special-attack'] ?? 50,
              spDef: stats['special-defense'] ?? 50,
              _raw: detail,
            }
          })
        )
        return details
      } catch (error) {
        console.log(error);
      }
      this.loading = false
    },


②次のポケモンの読み込み

さらに、次のポケモンを読み込む処理の考え方も非常に学びになりました。


先ほどの pokemonApi 関数には、引数として offset と limit を渡していますが、

これは「次のポケモンを取得するための範囲」を指定するためのものです。


まず、初回のAPI呼び出しでは以下のようにリクエストしています。

 data() {
   return {
     offset: 0,
     limit: 48,
   }
 },
 const listUrl = `https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`

→ このとき、1〜48番目のポケモンが取得されます。


その後、loadMore ボタンがクリックされると、

this.offset = this.offset + this.limit

という処理が走り、offset が 0 + 48 → 48 になります。


次のAPI呼び出しはこうなります

https://pokeapi.co/api/v2/pokemon?offset=48&limit=48

これにより、49番目から96番目までの48体が新たに取得され、

すでにある一覧の後ろに追加されていきます。


「こうやって 次へ の仕組みは作るのか!」と、

実際に実装しながらとても感動しました。


③モーダルを開くまでの流れ


子コンポーネント(カード側)

まず、カードコンポーネント(子)でポケモンをクリックすると、

そのクリックイベントで選択されたポケモンオブジェクトが emit されます

<div
  class="card"
  :data-type="pokemon.types[0] || 'normal'"
  @click="openModal(pokemon)"
>
  <!-- ポケモンカードの中身 -->
</div>

<script>
export default {
  methods: {
    async openModal(p) {
      this.$emit("select-pokemon", p)
    }
  }
}
</script>

この関数は、クリックされたポケモン p(1体分のオブジェクト)を

親コンポーネントに渡す(emitする)だけのシンプルな関数です。


親コンポーネント(受け取り側)

次に、親コンポーネントで子から渡されたデータを受け取ります

<div v-else class="grid">
  <pokemon-card
    v-for="pokemon in search_pokemons"
    :key="pokemon.id"
    class="card"
    :pokemon="pokemon"
    @select-pokemon="openModal"
  />
</div>

ここで、

子から emit された select-pokemon イベントを

@select-pokemon="openModal" で受け取り、

親側の openModal() が実行されます。


親の openModal() の処理

async openModal(p) {
  this.selected = p        // 一覧でクリックしたポケモンの整形済みデータ
  this.details = p._raw    // その中にある “APIの生データ” を別で格納
  this.isModalOpen = true  // モーダルを開くトリガー
}

ここでは、クリックしたポケモン p を

this.selected に格納し、

その中に含まれている _raw(PokéAPIの生データ)を this.details に格納します。


そして isModalOpen = true にすることで、モーダルを開くトリガーが発火します。


モーダルへのデータ受け渡し

親で保持している selected と details を

モーダルコンポーネントに props で渡します

<transition name="pop" appear>
  <app-modal
    :selected="selected"
    :details="details"
    @close-modal="closeModal"
  />
</transition>


モーダル側で受け取る

モーダル(子)コンポーネントでは、

props として受け取ったデータを利用します

props: {
  selected: Object,
  details: Object,
}

これで、

  • selected … 一覧で使っていた整形済みデータ(名前・タイプ・画像など)
  • details … PokéAPIから取得した生データ(ステータス・技など)

の両方をモーダル内で自由に扱うことができます。


④検索機能

検索機能に関しては、前回の JSONPlaceholder の API を使ったときに実装した方法をそのまま応用しました。

computed: {
  search_pokemons() {
    let searchWord = this.search.trim()
    if (searchWord === '') {
      return this.pokemons
    }
    return this.pokemons.filter(p =>
      p.name.toLowerCase().includes(searchWord.toLowerCase())
    )
  }
}

実装のときに意識したのは、なるべく自分の記憶を頼りに書くこと

忘れてしまった部分は、過去の記事や参考にした Qiita を見返しました。

そうやって少しずつ体に覚えさせていくような感覚で、コーディングしています。


まとめ


今回のPokéAPIアプリでは、

APIの二段階フェッチ や 次のデータの読み込み処理

そして モーダル実装前回学んだ検索機能の追加 など、

たくさんの機能を組み合わせて実装しました。


本当は「お気に入り機能」なども入れたかったのですが、

時間的にも実装量的にもかなり大きくなりそうだったため、

今回はここでいったん完成としています。


今はとにかく “量をこなして慣れること” を意識していて、

これからもどんどん小さなアプリを作りながら、

技術を体に染み込ませていきたいと思っています。


次の課題としては、やはり以下の2点かなと思っています。

  • Vuexを使った状態管理
  • ローカルストレージを利用したお気に入り保存機能

これらは実務でもよく使う技術なので、

しっかり理解を深めていきたいと思っています。


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