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 アプリケーションを構築できるのです。
関連リンク
articleNuxt 500 の犯人はどこ?server/api で起きる例外・CORS・runtimeConfig の切り分け
articleNuxt Nitro のしくみを図解で理解:サーバーレス実行とアダプタの舞台裏
articleNuxt 本番運用チェックリスト:セキュリティヘッダー・CSP・Cookie 設定を総点検
articleNuxt クリーンアーキテクチャ実践:UI・ドメイン・インフラを composable で分離
articleNuxt nuxi コマンド速見表:プロジェクト作成からモジュール公開まで
articleNuxt を macOS + yarn で最短構築:ESLint/Prettier/TS 設定まで一気通貫
articleWebSocket が「200 OK で Upgrade されない」原因と対処:プロキシ・ヘッダー・TLS の落とし穴
articleWebRTC 本番運用の SLO 設計:接続成功率・初画出し時間・通話継続率の基準値
articleAstro のレンダリング戦略を一望:MPA× 部分ハイドレーションの強みを図解解説
articleWebLLM が読み込めない時の原因と解決策:CORS・MIME・パス問題を総点検
articleVitest ESM/CJS 混在で `Cannot use import statement outside a module` が出る技術対処集
articleテスト環境比較:Vitest vs Jest vs Playwright CT ― Vite プロジェクトの最適解
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来