Nuxt で API 連携:fetch, useAsyncData, useFetch の違いと使い分け - 記事構成案

Nuxt で API 連携を行う際の 3 つの主要な方法について解説します。それぞれの特徴と使い分けを理解することで、効率的なデータ取得が可能になります。
現代の Web 開発では、データの取得と表示がアプリケーションの核となります。Nuxt が提供する 3 つの方法を正しく使い分けることで、パフォーマンスとユーザー体験を劇的に向上させることができるのです。
背景
モダンな Web アプリケーションでは、サーバーサイドとフロントエンドの連携が不可欠です。Nuxt では複数のデータ取得方法が提供されており、適切な選択が重要です。
Nuxt 3 では Composition API ベースの新しいデータ取得 API が導入され、より直感的で効率的な開発が可能になりました。しかし、選択肢が増えたことで「どれを使えばよいか」という迷いも生まれています。
実際のプロジェクトでは、要件やパフォーマンスの要求によって最適な方法が変わります。この記事では、それぞれの特徴を深く理解し、状況に応じた最適な選択ができるようになることを目指します。
課題
- どの方法をいつ使えばよいかわからない
- パフォーマンスの違いが理解できない
- SSR と CSR での動作の違いが不明確
多くの開発者が直面する問題は、3 つの方法の違いが曖昧で、適切な使い分けができないことです。特に以下のような場面で迷いが生じます:
- 初回ロード時のデータ取得
- ユーザーアクションに応じた動的データ取得
- エラーハンドリングの実装
- ローディング状態の管理
これらの課題を解決するためには、各方法の内部動作と特性を理解することが不可欠です。
解決策
fetch の基本と特徴
fetch
は Nuxt 3 で最も基本的なデータ取得方法です。ブラウザ標準の fetch
API を拡張したもので、SSR と CSR の両方で動作します。
基本的な使用方法
typescript// pages/index.vue
<script setup>
// 基本的な fetch の使用例
const { data: posts } = await useFetch('/api/posts')
// オプションを指定した fetch
const { data: user, error } = await useFetch('/api/user', {
method: 'POST',
body: { id: 1 },
headers: {
'Content-Type': 'application/json'
}
})
</script>
fetch の特徴と制限
fetch
の最大の特徴は、そのシンプルさと柔軟性です。しかし、いくつかの制限もあります:
typescript// fetch の制限例:型安全性が不十分
const { data } = await useFetch('/api/posts');
// data の型は any になるため、型安全性が低い
// エラーハンドリングが手動
const { data, error } = await useFetch('/api/posts');
if (error.value) {
console.error('データ取得に失敗しました:', error.value);
}
useAsyncData の活用方法
useAsyncData
は、より高度な制御が必要な場合に使用するデータ取得方法です。キャッシュ機能や型安全性を提供します。
基本的な使用方法
typescript// composables/usePosts.ts
export const usePosts = () => {
return useAsyncData('posts', async () => {
const response = await $fetch('/api/posts');
return response;
});
};
高度な機能の活用
useAsyncData
の真価は、その高度な制御機能にあります:
typescript// ページコンポーネントでの使用
<script setup>
const { data: posts, pending, error, refresh } = await useAsyncData(
'posts',
async () => {
const response = await $fetch('/api/posts')
return response
},
{
// キャッシュの設定
server: true,
client: true,
// エラーハンドリング
default: () => [],
// 変換処理
transform: (posts) => posts.map(post => ({
...post,
publishedAt: new Date(post.publishedAt)
}))
}
)
</script>
エラーハンドリングの実装
実際のプロジェクトでは、エラーハンドリングが重要になります:
typescript// エラーハンドリングの実装例
const { data, error, pending } = await useAsyncData(
'user-data',
async () => {
try {
const response = await $fetch('/api/user/profile');
return response;
} catch (err) {
// カスタムエラーオブジェクトを作成
throw createError({
statusCode: 500,
statusMessage: 'ユーザー情報の取得に失敗しました',
fatal: true,
});
}
}
);
// エラー状態の監視
watch(error, (newError) => {
if (newError) {
console.error('データ取得エラー:', newError);
// ユーザーへの通知処理
}
});
useFetch の利便性
useFetch
は、useAsyncData
と $fetch
を組み合わせた便利な関数です。最も直感的で使いやすい API を提供します。
基本的な使用方法
typescript// 最もシンプルな使用方法
const { data: posts } = await useFetch('/api/posts');
// オプションを指定した使用
const {
data: user,
error,
pending,
} = await useFetch('/api/user', {
method: 'POST',
body: { email: 'user@example.com' },
headers: {
Authorization: `Bearer ${token}`,
},
});
型安全性の活用
useFetch
の最大の利点は、自動的な型推論です:
typescript// 型定義
interface Post {
id: number;
title: string;
content: string;
publishedAt: string;
}
interface User {
id: number;
name: string;
email: string;
}
// 型安全なデータ取得
const { data: posts } = await useFetch<Post[]>(
'/api/posts'
);
const { data: user } = await useFetch<User>('/api/user/1');
// TypeScript が型を自動推論
console.log(posts.value?.[0]?.title); // 型安全
console.log(user.value?.name); // 型安全
実際のプロジェクトでの活用例
typescript// 認証付き API 呼び出し
const { data: profile, error } = await useFetch(
'/api/profile',
{
// 認証トークンの自動付与
headers: {
Authorization: `Bearer ${useCookie('token').value}`,
},
// エラー時の処理
onResponseError({ response }) {
if (response.status === 401) {
// 認証エラーの場合、ログインページにリダイレクト
navigateTo('/login');
}
},
}
);
3 つの方法の比較表
項目 | fetch | useAsyncData | useFetch |
---|---|---|---|
シンプルさ | ★★★★★ | ★★★ | ★★★★★ |
型安全性 | ★★ | ★★★★★ | ★★★★★ |
キャッシュ制御 | ★★ | ★★★★★ | ★★★★ |
エラーハンドリング | ★★★ | ★★★★★ | ★★★★ |
SSR 対応 | ★★★★ | ★★★★★ | ★★★★★ |
学習コスト | ★★★★★ | ★★★ | ★★★★ |
具体例
基本的な API 呼び出し
実際のプロジェクトでよく使用されるパターンを紹介します。
ブログ記事一覧の取得
typescript// pages/blog/index.vue
<script setup>
// 記事一覧の取得
const { data: posts, pending, error } = await useFetch('/api/posts', {
// デフォルト値の設定
default: () => [],
// 変換処理
transform: (posts) => posts.map(post => ({
...post,
excerpt: post.content.substring(0, 150) + '...'
}))
})
</script>
<template>
<div>
<div v-if="pending" class="loading">
記事を読み込み中...
</div>
<div v-else-if="error" class="error">
記事の読み込みに失敗しました
</div>
<div v-else class="posts">
<article v-for="post in posts" :key="post.id" class="post">
<h2>{{ post.title }}</h2>
<p>{{ post.excerpt }}</p>
</article>
</div>
</div>
</template>
ユーザー認証とプロフィール取得
typescript// composables/useAuth.ts
export const useAuth = () => {
const user = ref(null);
const isAuthenticated = computed(() => !!user.value);
const login = async (credentials) => {
const { data, error } = await useFetch(
'/api/auth/login',
{
method: 'POST',
body: credentials,
}
);
if (error.value) {
throw new Error('ログインに失敗しました');
}
user.value = data.value;
return data.value;
};
const logout = async () => {
await useFetch('/api/auth/logout', { method: 'POST' });
user.value = null;
};
return {
user: readonly(user),
isAuthenticated,
login,
logout,
};
};
エラーハンドリング
実際のプロジェクトでは、適切なエラーハンドリングが重要です。
包括的なエラーハンドリング
typescript// utils/api.ts
export const createApiError = (error: any) => {
if (error.statusCode === 404) {
return {
message: 'リソースが見つかりません',
code: 'NOT_FOUND',
};
}
if (error.statusCode === 401) {
return {
message: '認証が必要です',
code: 'UNAUTHORIZED',
};
}
if (error.statusCode === 500) {
return {
message: 'サーバーエラーが発生しました',
code: 'SERVER_ERROR',
};
}
return {
message: '予期しないエラーが発生しました',
code: 'UNKNOWN_ERROR',
};
};
エラーハンドリングの実装例
typescript// pages/products/[id].vue
<script setup>
const route = useRoute()
const { data: product, error, pending } = await useFetch(
`/api/products/${route.params.id}`,
{
// エラー時の処理
onResponseError({ response }) {
const apiError = createApiError(response)
// エラーログの記録
console.error('Product fetch error:', apiError)
// ユーザーへの通知
if (apiError.code === 'NOT_FOUND') {
throw createError({
statusCode: 404,
statusMessage: '商品が見つかりません'
})
}
}
}
)
</script>
ローディング状態の管理
ユーザー体験を向上させるため、適切なローディング状態の管理が重要です。
スケルトンローディングの実装
typescript// components/PostSkeleton.vue
<template>
<div class="post-skeleton">
<div class="skeleton-title"></div>
<div class="skeleton-content">
<div class="skeleton-line"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
</div>
</div>
</template>
<style scoped>
.post-skeleton {
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
}
.skeleton-title {
height: 1.5rem;
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 0.25rem;
margin-bottom: 1rem;
}
.skeleton-line {
height: 1rem;
background: linear-gradient(90deg, #f3f4f6 25%, #e5e7eb 50%, #f3f4f6 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
border-radius: 0.25rem;
margin-bottom: 0.5rem;
}
.skeleton-line.short {
width: 60%;
}
@keyframes loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
ローディング状態の活用
typescript// pages/blog/index.vue
<script setup>
const { data: posts, pending, error } = await useFetch('/api/posts')
</script>
<template>
<div class="blog-container">
<h1>ブログ記事一覧</h1>
<!-- ローディング中 -->
<div v-if="pending" class="loading-container">
<PostSkeleton v-for="i in 3" :key="i" />
</div>
<!-- エラー時 -->
<div v-else-if="error" class="error-container">
<p>記事の読み込みに失敗しました</p>
<button @click="refresh">再試行</button>
</div>
<!-- データ表示 -->
<div v-else class="posts-container">
<article v-for="post in posts" :key="post.id" class="post">
<h2>{{ post.title }}</h2>
<p>{{ post.excerpt }}</p>
</article>
</div>
</div>
</template>
条件付きデータ取得
実際のアプリケーションでは、条件に応じてデータを取得する必要があります。
認証状態に応じたデータ取得
typescript// pages/dashboard.vue
<script setup>
const { user } = useAuth()
// 認証状態に応じてデータを取得
const { data: dashboardData } = await useFetch('/api/dashboard', {
// 認証されている場合のみ実行
server: false,
client: true,
// 条件付き実行
immediate: computed(() => !!user.value)
})
// ユーザーが未認証の場合は別のデータを取得
const { data: publicData } = await useFetch('/api/public-dashboard', {
immediate: computed(() => !user.value)
})
</script>
動的な API エンドポイント
typescript// pages/users/[id].vue
<script setup>
const route = useRoute()
// 動的なエンドポイントでのデータ取得
const { data: user, error } = await useFetch(
() => `/api/users/${route.params.id}`,
{
// パラメータが変更されたときに再取得
watch: [route.params.id],
// デフォルト値
default: () => ({
id: 0,
name: '',
email: ''
})
}
)
</script>
まとめ
Nuxt で API 連携を行う際の 3 つの方法について詳しく解説しました。それぞれの特徴を理解し、適切に使い分けることで、効率的で保守性の高いアプリケーションを構築できます。
重要なポイント
- シンプルな用途には
useFetch
:型安全性と使いやすさを両立 - 高度な制御には
useAsyncData
:キャッシュや変換処理が必要な場合 - 柔軟性重視には
fetch
:カスタマイズ性が重要な場合
実践的なアドバイス
実際のプロジェクトでは、一つの方法に固執するのではなく、用途に応じて適切に選択することが重要です。また、エラーハンドリングとローディング状態の管理を適切に行うことで、ユーザー体験を大幅に向上させることができます。
Nuxt のデータ取得 API を正しく使いこなすことで、パフォーマンスと保守性を両立したモダンな Web アプリケーションを構築できるのです。
関連リンク
- article
Jotai を 120%活用するユーティリティライブラリ(jotai-immer, jotai-xstate, jotai-form)の世界
- article
TypeScript × Vitest:次世代テストランナーの導入から活用まで
- article
スマホでも滑らか!React × アニメーションのレスポンシブ対応術
- article
Zustand でリストデータと詳細データを効率よく管理する方法
- article
Nuxt で API 連携:fetch, useAsyncData, useFetch の違いと使い分け - 記事構成案
- article
htmx の history 拡張で快適な SPA ライク体験を実現
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来