Fetch APIとAxiosによるデータ取得

2025-08-29

はじめに

前回までで、Express.jsとPrismaを使用した本格的なバックエンドAPIの構築が完了しました。今回は、ReactフロントエンドからこのAPIに接続し、データを取得・操作する方法を学びます。現代のWeb開発では、Fetch APIAxiosの2つの主要な方法でHTTP通信を行います。

Fetch API vs Axios

Fetch API

  • ネイティブAPI: 現代のブラウザに組み込まれている
  • Promiseベース: 非同期処理を簡潔に記述可能
  • 軽量: 追加ライブラリ不要

Axios

  • サードパーティライブラリ: より豊富な機能を提供
  • 自動JSON変換: レスポンスデータを自動でJSONに変換
  • リクエスト/レスポンスインターセプト: 通信前後の処理をフック可能
  • ブラウザ/Node.js両対応: クライアント/サーバー両方で使用可能

プロジェクト設定

Reactアプリケーションの作成(まだの場合)

npx create-react-app todo-frontend
cd todo-frontend
npm start

Axiosのインストール

npm install axios

APIクライアントの実装

基本設定ファイルの作成

// src/api/client.js
import axios from 'axios';

// APIのベースURL
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3000';

// Axiosインスタンスの作成
const apiClient = axios.create({
  baseURL: API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// リクエストインターセプター(後で認証トークンを追加します)
apiClient.interceptors.request.use(
  (config) => {
    // ここでリクエスト前の処理を追加
    console.log('リクエスト送信:', config.method, config.url);
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// レスポンスインターセプター
apiClient.interceptors.response.use(
  (response) => {
    console.log('レスポンス受信:', response.status, response.config.url);
    return response;
  },
  (error) => {
    console.error('APIエラー:', error.response?.status, error.message);

    // エラーハンドリング
    if (error.response?.status === 401) {
      // 未認証エラー(後で処理を追加)
      console.log('認証が必要です');
    } else if (error.response?.status === 404) {
      console.log('リソースが見つかりません');
    }

    return Promise.reject(error);
  }
);

export default apiClient;

APIサービス層の実装

// src/api/todoService.js
import apiClient from './client';

// TODO一覧の取得
export const fetchTodos = async () => {
  try {
    const response = await apiClient.get('/todos');
    return response.data;
  } catch (error) {
    console.error('TODO一覧の取得に失敗しました:', error);
    throw error;
  }
};

// 特定のTODOの取得
export const fetchTodoById = async (id) => {
  try {
    const response = await apiClient.get(`/todos/${id}`);
    return response.data;
  } catch (error) {
    console.error(`TODO(ID: ${id})の取得に失敗しました:`, error);
    throw error;
  }
};

// TODOの新規作成
export const createTodo = async (todoData) => {
  try {
    const response = await apiClient.post('/todos', todoData);
    return response.data;
  } catch (error) {
    console.error('TODOの作成に失敗しました:', error);
    throw error;
  }
};

// TODOの更新
export const updateTodo = async (id, todoData) => {
  try {
    const response = await apiClient.put(`/todos/${id}`, todoData);
    return response.data;
  } catch (error) {
    console.error(`TODO(ID: ${id})の更新に失敗しました:`, error);
    throw error;
  }
};

// TODOの削除
export const deleteTodo = async (id) => {
  try {
    await apiClient.delete(`/todos/${id}`);
  } catch (error) {
    console.error(`TODO(ID: ${id})の削除に失敗しました:`, error);
    throw error;
  }
};

// 統計情報の取得
export const fetchTodoStats = async () => {
  try {
    const response = await apiClient.get('/todos/stats');
    return response.data;
  } catch (error) {
    console.error('統計情報の取得に失敗しました:', error);
    throw error;
  }
};

// ページネーション付きTODO一覧の取得
export const fetchPaginatedTodos = async (page = 1, limit = 10) => {
  try {
    const response = await apiClient.get(`/todos/paginated?page=${page}&limit=${limit}`);
    return response.data;
  } catch (error) {
    console.error('ページネーション付きTODO一覧の取得に失敗しました:', error);
    throw error;
  }
};

Fetch APIを使用した実装例

Axiosだけでなく、ネイティブのFetch APIを使用する方法も学びましょう。

// src/api/todoServiceFetch.js
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3000';

// 汎用HTTPリクエスト関数
const request = async (endpoint, options = {}) => {
  const url = `${API_BASE_URL}${endpoint}`;
  const defaultOptions = {
    headers: {
      'Content-Type': 'application/json',
    },
  };

  const config = { ...defaultOptions, ...options };

  try {
    const response = await fetch(url, config);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    // 204 No Contentの場合はデータをパースしない
    if (response.status === 204) {
      return null;
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('APIリクエストエラー:', error);
    throw error;
  }
};

// TODO一覧の取得(Fetch API版)
export const fetchTodosWithFetch = async () => {
  return request('/todos');
};

// TODOの新規作成(Fetch API版)
export const createTodoWithFetch = async (todoData) => {
  return request('/todos', {
    method: 'POST',
    body: JSON.stringify(todoData),
  });
};

// TODOの更新(Fetch API版)
export const updateTodoWithFetch = async (id, todoData) => {
  return request(`/todos/${id}`, {
    method: 'PUT',
    body: JSON.stringify(todoData),
  });
};

// TODOの削除(Fetch API版)
export const deleteTodoWithFetch = async (id) => {
  return request(`/todos/${id}`, {
    method: 'DELETE',
  });
};

カスタムフックの作成

Reactのカスタムフックを使用して、API通信のロジックを再利用可能にします。

// src/hooks/useTodos.js
import { useState, useEffect } from 'react';
import { 
  fetchTodos, 
  createTodo, 
  updateTodo, 
  deleteTodo,
  fetchTodoStats 
} from '../api/todoService';

export const useTodos = () => {
  const [todos, setTodos] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [stats, setStats] = useState(null);

  // TODO一覧の取得
  const getTodos = async () => {
    setLoading(true);
    setError(null);
    try {
      const data = await fetchTodos();
      setTodos(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  // 統計情報の取得
  const getStats = async () => {
    try {
      const data = await fetchTodoStats();
      setStats(data);
    } catch (err) {
      console.error('統計情報の取得に失敗しました:', err);
    }
  };

  // TODOの新規作成
  const addTodo = async (title) => {
    try {
      const newTodo = await createTodo({ title, completed: false });
      setTodos(prev => [...prev, newTodo]);
      return newTodo;
    } catch (err) {
      setError(err.message);
      throw err;
    }
  };

  // TODOの更新
  const toggleTodo = async (id, completed) => {
    try {
      const updatedTodo = await updateTodo(id, { completed });
      setTodos(prev => 
        prev.map(todo => todo.id === id ? updatedTodo : todo)
      );
      return updatedTodo;
    } catch (err) {
      setError(err.message);
      throw err;
    }
  };

  // TODOの削除
  const removeTodo = async (id) => {
    try {
      await deleteTodo(id);
      setTodos(prev => prev.filter(todo => todo.id !== id));
    } catch (err) {
      setError(err.message);
      throw err;
    }
  };

  // コンポーネントマウント時にデータ取得
  useEffect(() => {
    getTodos();
    getStats();
  }, []);

  return {
    todos,
    stats,
    loading,
    error,
    addTodo,
    toggleTodo,
    removeTodo,
    refreshTodos: getTodos,
    refreshStats: getStats,
  };
};

Reactコンポーネントでの使用例

// src/components/TodoList.js
import React, { useState } from 'react';
import { useTodos } from '../hooks/useTodos';

const TodoList = () => {
  const { todos, stats, loading, error, addTodo, toggleTodo, removeTodo } = useTodos();
  const [newTodoTitle, setNewTodoTitle] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!newTodoTitle.trim()) return;

    try {
      await addTodo(newTodoTitle);
      setNewTodoTitle('');
    } catch (err) {
      // エラー処理はuseTodos内で行われます
    }
  };

  const handleToggle = async (id, completed) => {
    try {
      await toggleTodo(id, !completed);
    } catch (err) {
      // エラー処理
    }
  };

  const handleDelete = async (id) => {
    try {
      await removeTodo(id);
    } catch (err) {
      // エラー処理
    }
  };

  if (loading && todos.length === 0) {
    return <div className="loading">読み込み中...</div>;
  }

  if (error) {
    return <div className="error">エラー: {error}</div>;
  }

  return (
    <div className="todo-app">
      <h1>TODOアプリ</h1>

      {stats && (
        <div className="stats">
          <p>総数: {stats.total} | 完了: {stats.completed} | 未完了: {stats.pending}</p>
        </div>
      )}

      <form onSubmit={handleSubmit} className="todo-form">
        <input
          type="text"
          value={newTodoTitle}
          onChange={(e) => setNewTodoTitle(e.target.value)}
          placeholder="新しいTODOを入力..."
          className="todo-input"
        />
        <button type="submit" className="add-button">追加</button>
      </form>

      <ul className="todo-list">
        {todos.map(todo => (
          <li key={todo.id} className={`todo-item ${todo.completed ? 'completed' : ''}`}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggle(todo.id, todo.completed)}
              className="todo-checkbox"
            />
            <span className="todo-title">{todo.title}</span>
            <button
              onClick={() => handleDelete(todo.id)}
              className="delete-button"
            >
              削除
            </button>
          </li>
        ))}
      </ul>

      {todos.length === 0 && !loading && (
        <p className="empty-message">TODOがありません</p>
      )}
    </div>
  );
};

export default TodoList;

エラーハンドリングの改善

より堅牢なエラーハンドリングを実装します。

// src/utils/errorHandler.js
export class ApiError extends Error {
  constructor(message, statusCode, details = null) {
    super(message);
    this.name = 'ApiError';
    this.statusCode = statusCode;
    this.details = details;
  }
}

export const handleApiError = (error) => {
  if (error.response) {
    // サーバーからのエラーレスポンス
    throw new ApiError(
      error.response.data?.error || 'サーバーエラーが発生しました',
      error.response.status,
      error.response.data
    );
  } else if (error.request) {
    // リクエストは送信されたがレスポンスがない
    throw new ApiError('サーバーとの接続に失敗しました', 0);
  } else {
    // リクエスト設定中のエラー
    throw new ApiError(error.message, 0);
  }
};

// サービス層での使用例
export const enhancedFetchTodos = async () => {
  try {
    const response = await apiClient.get('/todos');
    return response.data;
  } catch (error) {
    throw handleApiError(error);
  }
};

次のステップ

Fetch APIとAxiosを使用したフロントエンドとバックエンドの連携が完成しました。次回は、JWT認証を実装して、ユーザーごとにTODOを管理できるようにします。これにより、アプリケーションのセキュリティと個人化が向上します。

まとめ

今回学んだ内容:

  1. Fetch APIとAxiosの比較: それぞれの特徴と使い分け
  2. APIクライアントの設定: ベースURL、インターセプターの設定
  3. サービス層の実装: API呼び出しをカプセル化
  4. カスタムフックの作成: 再利用可能なデータ取得ロジック
  5. エラーハンドリング: ユーザーフレンドリーなエラー表示

これらの技術を組み合わせることで、ReactアプリケーションからバックエンドAPIとシームレスに連携する方法を習得しました。次回はセキュリティを強化する認証機能を実装します。