こんにちは、かつコーチです。
前回は「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.dataをrefに代入するだけで画面に反映されます。
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>
isLoadingとerrorMessageという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を活用する
