T-CREATOR

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

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 つの方法の比較表

項目fetchuseAsyncDatauseFetch
シンプルさ★★★★★★★★★★★★★
型安全性★★★★★★★★★★★★
キャッシュ制御★★★★★★★★★★★
エラーハンドリング★★★★★★★★★★★★
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 つの方法について詳しく解説しました。それぞれの特徴を理解し、適切に使い分けることで、効率的で保守性の高いアプリケーションを構築できます。

重要なポイント

  1. シンプルな用途には useFetch:型安全性と使いやすさを両立
  2. 高度な制御には useAsyncData:キャッシュや変換処理が必要な場合
  3. 柔軟性重視には fetch:カスタマイズ性が重要な場合

実践的なアドバイス

実際のプロジェクトでは、一つの方法に固執するのではなく、用途に応じて適切に選択することが重要です。また、エラーハンドリングとローディング状態の管理を適切に行うことで、ユーザー体験を大幅に向上させることができます。

Nuxt のデータ取得 API を正しく使いこなすことで、パフォーマンスと保守性を両立したモダンな Web アプリケーションを構築できるのです。

関連リンク