【Vue】axiosでAPIからデータを取得して表示する

JavaScript

こんにちは、かつコーチです。

前回は「Piniaの実践:ストアの分割と設計」で、状態管理の設計パターンを扱いました。

今回からは、実際にAPIと通信してデータを取得・表示する「データ連携編」に入っていきます。

第一弾は、Vueアプリで定番のHTTPクライアントであるaxiosを使ったAPI連携です。

axiosとは?なぜVueプロジェクトでよく使われるのか

axiosの特徴

axiosは、ブラウザやNode.jsでHTTP通信を行うためのJavaScriptライブラリです。

Promiseベースで書けるうえ、リクエスト・レスポンスの共通設定をまとめられる「インスタンス」の仕組みが用意されています。

JSONデータも自動的にパースしてくれるので、response.dataと書くだけで欲しいデータにアクセスできます。

標準のfetchとの違い

JavaScriptには標準でfetch関数が用意されていますが、Vueの実務では今もaxiosが選ばれる場面が多いです。

  • fetchはレスポンスが失敗ステータス(404や500)でも自動では例外を投げないため、response.okを毎回チェックする必要がある
  • axiosはエラーステータスで自動的に例外を投げてくれるので、try/catchにまとめやすい
  • axiosは共通ヘッダーやベースURLを「インスタンス」としてまとめて管理できる

小規模な個人アプリならfetchだけでも十分ですが、複数のAPIエンドポイントを呼ぶ中〜大規模なアプリでは、axiosの方が管理がラクになる場面が多いです。

Vueプロジェクトへのaxios導入

インストールと基本の使い方

まずはプロジェクトにaxiosを追加します。

npm install axios

もっともシンプルな使い方は、コンポーネント内で直接importして呼び出す形です。

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'

interface User {
  id: number
  name: string
  email: string
}

const users = ref<User[]>([])

onMounted(async () => {
  const response = await axios.get<User[]>('https://api.example.com/users')
  users.value = response.data
})
</script>

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      {{ user.name }}({{ user.email }})
    </li>
  </ul>
</template>

onMountedの中でawait axios.get()を呼び、返ってきたresponse.datarefに代入するだけで画面に反映されます。

axiosインスタンスを共通化する

APIのベースURLや共通ヘッダーは、毎回書くとミスの元になります。

src/lib/api.tsのようなファイルにaxiosインスタンスをまとめておくと、以降のリクエストがすっきりします。

// src/lib/api.ts
import axios from 'axios'

export const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
  },
})

こうしておけば、コンポーネント側ではapiClient.get('/users')のようにパスだけ書けば済みます。

ローディング・エラー状態を含めて表示する

3つの状態を管理する

実務でAPI連携を書くときは、データそのものだけでなく「ローディング中かどうか」「エラーが起きたかどうか」もあわせて管理するのが基本です。

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { apiClient } from '@/lib/api'

interface Post {
  id: number
  title: string
}

const posts = ref<Post[]>([])
const isLoading = ref(true)
const errorMessage = ref('')

const fetchPosts = async () => {
  isLoading.value = true
  errorMessage.value = ''
  try {
    const response = await apiClient.get<Post[]>('/posts')
    posts.value = response.data
  } catch (error) {
    errorMessage.value = '記事の取得に失敗しました'
  } finally {
    isLoading.value = false
  }
}

onMounted(fetchPosts)
</script>

<template>
  <p v-if="isLoading">読み込み中です...</p>
  <p v-else-if="errorMessage">{{ errorMessage }}</p>
  <ul v-else>
    <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
  </ul>
</template>

isLoadingerrorMessageという2つの状態を用意しておくことで、通信中・失敗時・成功時のすべてのパターンを画面に反映できます。

よくあるつまずきポイント:エラーハンドリングの書き忘れ

私が実際にやらかした失敗

私が業務でAPI連携を実装し始めたばかりの頃、try/catchを書かずにawait axios.get()だけ書いていた時期がありました。

普段は問題なく動くのですが、APIサーバーが一時的にダウンしたタイミングで画面が真っ白になり、コンソールにはUncaught (in promise)というエラーだけが残る、という状況に遭遇しました。

ユーザーには何が起きたのか一切伝わらず、原因調査にも時間がかかってしまった苦い経験です。

❌ Before:エラーハンドリングがなく、失敗時に何も表示されない

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { apiClient } from '@/lib/api'

const posts = ref<{ id: number; title: string }[]>([])

onMounted(async () => {
  const response = await apiClient.get('/posts')
  posts.value = response.data
  // APIがエラーを返すと、この行で処理が止まり
  // 画面には何のフィードバックも表示されない
})
</script>

✅ After:try/catchでエラーを捕まえ、ユーザーに状態を伝える

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { apiClient } from '@/lib/api'

const posts = ref<{ id: number; title: string }[]>([])
const errorMessage = ref('')

onMounted(async () => {
  try {
    const response = await apiClient.get('/posts')
    posts.value = response.data
  } catch (error) {
    errorMessage.value = '記事の取得に失敗しました。時間をおいて再度お試しください'
    console.error(error) // 開発中はコンソールにも詳細を残す
  }
})
</script>

<template>
  <p v-if="errorMessage" class="error">{{ errorMessage }}</p>
</template>

APIとの通信は「必ず失敗する可能性がある処理」だと考え、try/catchとエラー時の表示をセットで実装する習慣をつけましょう。

応用:更新系リクエストとコンポーザブル化

POST・PUT・DELETEも基本は同じ

データの登録・更新・削除も、メソッドが変わるだけで書き方の骨格は同じです。

// 新規投稿の作成
await apiClient.post('/posts', { title: '新しい記事' })

// 既存投稿の更新
await apiClient.put(`/posts/${postId}`, { title: '更新後のタイトル' })

// 投稿の削除
await apiClient.delete(`/posts/${postId}`)

useXxxという名前でコンポーザブルにまとめる

同じようなデータ取得処理を複数のコンポーネントで使う場合は、composables/usePosts.tsのような形でロジックを切り出すと再利用性が高まります。

// composables/usePosts.ts
import { ref } from 'vue'
import { apiClient } from '@/lib/api'

export function usePosts() {
  const posts = ref<{ id: number; title: string }[]>([])
  const isLoading = ref(false)
  const errorMessage = ref('')

  const fetchPosts = async () => {
    isLoading.value = true
    errorMessage.value = ''
    try {
      const response = await apiClient.get('/posts')
      posts.value = response.data
    } catch (error) {
      errorMessage.value = '記事の取得に失敗しました'
    } finally {
      isLoading.value = false
    }
  }

  return { posts, isLoading, errorMessage, fetchPosts }
}

こうしておけば、複数のコンポーネントからconst { posts, isLoading, fetchPosts } = usePosts()と呼ぶだけで、同じ取得ロジックを使い回せます。

次回扱う「VueUse」も、この考え方をさらに一歩進めたComposableの集合体です。

まとめ

この記事のポイント

  • axiosはPromiseベースでエラーハンドリングがしやすく、共通設定をインスタンス化できる
  • データ取得時は「データ本体」「ローディング状態」「エラー状態」の3つをセットで管理する
  • try/catchを書かないと、通信失敗時にユーザーへ何も伝わらない事故につながる
  • 更新系リクエスト(POST/PUT/DELETE)も基本の書き方は取得系と同じ
  • 同じ取得ロジックはComposableに切り出すと再利用しやすくなる

次に読むべき記事

次回は「VueUseで便利なComposableを活用する」を解説します。

今回自作したような処理も、VueUseを使うとさらに簡潔に書けるようになります。

→ 次の記事:VueUseで便利なComposableを活用する

タイトルとURLをコピーしました