T-CREATOR

Svelte で API 通信を極める:fetch 活用ガイド

Svelte で API 通信を極める:fetch 活用ガイド

Svelte は軽量で高速なフロントエンドフレームワークとして注目を集めています。特に API 通信において、そのシンプルさとパフォーマンスの高さが際立っています。

この記事では、Svelte での API 通信を極めるための実践的なテクニックを、fetch API を中心に詳しく解説していきます。初心者の方でも理解しやすいように、段階的に学べる構成になっています。

Svelte での API 通信の基礎

Svelte の特徴と API 通信の相性

Svelte は他のフレームワークと比べて、API 通信において独特の利点があります。まずは、なぜ Svelte が API 通信に適しているのかを理解しましょう。

リアクティブな更新の仕組み Svelte の最大の特徴は、そのリアクティブシステムです。変数の代入だけで自動的に DOM が更新されるため、API から取得したデータを簡単に画面に反映できます。

javascript// Svelteのリアクティブシステムの例
let userData = null;

// APIからデータを取得して代入するだけで画面が更新される
async function fetchUserData() {
  const response = await fetch('/api/user');
  userData = await response.json();
}

このシンプルさが、Svelte での API 通信を直感的で理解しやすくしています。

ブラウザ標準の fetch API の活用

Svelte では特別なライブラリを導入せずに、ブラウザ標準の fetch API を活用できます。これにより、バンドルサイズを抑えながら、強力な API 通信機能を実現できます。

fetch API の基本構造

javascript// 基本的なfetchの使い方
const response = await fetch(url, {
  method: 'GET', // HTTPメソッド
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(data), // POST/PUTの場合
});

Svelte のリアクティブシステムとの連携

Svelte のリアクティブシステムと fetch API を組み合わせることで、非常に効率的なデータ管理が可能になります。

javascript// Svelteコンポーネントでの実装例
<script>
    import { onMount } from 'svelte';

    let posts = [];
    let loading = false;
    let error = null;

    onMount(async () => {
        await loadPosts();
    });

    async function loadPosts() {
        loading = true;
        error = null;

        try {
            const response = await fetch('/api/posts');
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            posts = await response.json();
        } catch (err) {
            error = err.message;
        } finally {
            loading = false;
        }
    }
</script>

fetch API の基本操作

GET リクエストの実装

GET リクエストは、データを取得する最も基本的な操作です。Svelte では、この操作を非常にシンプルに実装できます。

シンプルな GET リクエスト

javascript// 基本的なGETリクエスト
async function fetchData() {
  try {
    const response = await fetch(
      'https://api.example.com/data'
    );

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

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('データの取得に失敗しました:', error);
    throw error;
  }
}

クエリパラメータ付きの GET リクエスト

javascript// URLSearchParamsを使ったクエリパラメータの構築
function buildUrl(baseUrl, params) {
  const url = new URL(baseUrl);
  Object.keys(params).forEach((key) => {
    url.searchParams.append(key, params[key]);
  });
  return url.toString();
}

async function fetchUsers(page = 1, limit = 10) {
  const url = buildUrl('/api/users', { page, limit });
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(
      `ユーザー取得エラー: ${response.status}`
    );
  }

  return await response.json();
}

POST/PUT/DELETE リクエストの実装

データの作成、更新、削除を行うリクエストの実装方法を学びましょう。

POST リクエスト(データ作成)

javascript// 新しいユーザーを作成するPOSTリクエスト
async function createUser(userData) {
  try {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify(userData),
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(
        errorData.message || 'ユーザー作成に失敗しました'
      );
    }

    return await response.json();
  } catch (error) {
    console.error('ユーザー作成エラー:', error);
    throw error;
  }
}

PUT リクエスト(データ更新)

javascript// ユーザー情報を更新するPUTリクエスト
async function updateUser(userId, updateData) {
  try {
    const response = await fetch(`/api/users/${userId}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
      },
      body: JSON.stringify(updateData),
    });

    if (!response.ok) {
      throw new Error(`更新エラー: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error('ユーザー更新エラー:', error);
    throw error;
  }
}

DELETE リクエスト(データ削除)

javascript// ユーザーを削除するDELETEリクエスト
async function deleteUser(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`, {
      method: 'DELETE',
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });

    if (!response.ok) {
      throw new Error(`削除エラー: ${response.status}`);
    }

    return true; // 削除成功
  } catch (error) {
    console.error('ユーザー削除エラー:', error);
    throw error;
  }
}

エラーハンドリングの基本

API 通信では、エラーハンドリングが非常に重要です。適切なエラー処理により、ユーザーエクスペリエンスを大幅に向上させることができます。

包括的なエラーハンドリング

javascript// エラーハンドリングを含むAPI呼び出し関数
async function apiCall(url, options = {}) {
  try {
    const response = await fetch(url, {
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      ...options,
    });

    // HTTPステータスコードのチェック
    if (!response.ok) {
      let errorMessage = `HTTP ${response.status}: ${response.statusText}`;

      // エラーレスポンスの詳細を取得
      try {
        const errorData = await response.json();
        errorMessage = errorData.message || errorMessage;
      } catch {
        // JSONパースに失敗した場合はデフォルトメッセージを使用
      }

      throw new Error(errorMessage);
    }

    // レスポンスが空の場合はnullを返す
    const contentType =
      response.headers.get('content-type');
    if (
      contentType &&
      contentType.includes('application/json')
    ) {
      return await response.json();
    }

    return await response.text();
  } catch (error) {
    // ネットワークエラーの処理
    if (
      error.name === 'TypeError' &&
      error.message.includes('fetch')
    ) {
      throw new Error(
        'ネットワーク接続エラー: インターネット接続を確認してください'
      );
    }

    // タイムアウトエラーの処理
    if (error.name === 'AbortError') {
      throw new Error('リクエストがタイムアウトしました');
    }

    throw error;
  }
}

よくあるエラーコードとその対処法

エラーコード意味対処法
400Bad Requestリクエストの形式を確認
401Unauthorized認証トークンの確認
403Forbidden権限の確認
404Not FoundURL とリソースの確認
429Too Many Requestsレート制限の確認
500Internal Server Errorサーバー側の問題

Svelte での状態管理と API 通信

ストアを使ったデータ管理

Svelte のストア機能を活用することで、API 通信の状態を効率的に管理できます。

基本的なストアの実装

javascript// stores/api.js
import { writable } from 'svelte/store';

// ユーザーデータのストア
export const users = writable([]);
export const usersLoading = writable(false);
export const usersError = writable(null);

// ユーザー取得のアクション
export async function fetchUsers() {
  usersLoading.set(true);
  usersError.set(null);

  try {
    const response = await fetch('/api/users');
    if (!response.ok) {
      throw new Error(
        `HTTP ${response.status}: ${response.statusText}`
      );
    }

    const data = await response.json();
    users.set(data);
  } catch (error) {
    usersError.set(error.message);
    console.error('ユーザー取得エラー:', error);
  } finally {
    usersLoading.set(false);
  }
}

ストアを使用したコンポーネント

javascript<!-- UserList.svelte -->
<script>
    import { users, usersLoading, usersError, fetchUsers } from './stores/api.js';
    import { onMount } from 'svelte';

    onMount(() => {
        fetchUsers();
    });
</script>

{#if $usersLoading}
    <div class="loading">読み込み中...</div>
{:else if $usersError}
    <div class="error">エラー: {$usersError}</div>
{:else}
    <ul>
        {#each $users as user}
            <li>{user.name} - {user.email}</li>
        {/each}
    </ul>
{/if}

ローディング状態の管理

ユーザーエクスペリエンスを向上させるため、適切なローディング状態の管理が重要です。

詳細なローディング状態管理

javascript// stores/loading.js
import { writable } from 'svelte/store';

// 個別のAPI呼び出しのローディング状態
export const loadingStates = writable(new Map());

// ローディング状態を設定する関数
export function setLoading(key, isLoading) {
  loadingStates.update((states) => {
    const newStates = new Map(states);
    if (isLoading) {
      newStates.set(key, true);
    } else {
      newStates.delete(key);
    }
    return newStates;
  });
}

// 特定の操作がローディング中かどうかを確認
export function isLoading(key) {
  let currentStates;
  loadingStates.subscribe((states) => {
    currentStates = states;
  })();
  return currentStates.has(key);
}

ローディング状態を使用した実装例

javascript// ユーザー作成時のローディング状態管理
async function createUserWithLoading(userData) {
  const loadingKey = 'createUser';
  setLoading(loadingKey, true);

  try {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData),
    });

    if (!response.ok) {
      throw new Error(`作成エラー: ${response.status}`);
    }

    const newUser = await response.json();
    // 成功時の処理
    return newUser;
  } catch (error) {
    console.error('ユーザー作成エラー:', error);
    throw error;
  } finally {
    setLoading(loadingKey, false);
  }
}

エラー状態の管理

エラー状態を適切に管理することで、ユーザーに分かりやすいフィードバックを提供できます。

エラー状態管理の実装

javascript// stores/error.js
import { writable } from 'svelte/store';

// エラー状態のストア
export const errors = writable(new Map());

// エラーを設定する関数
export function setError(key, error) {
  errors.update((errorMap) => {
    const newErrors = new Map(errorMap);
    newErrors.set(key, {
      message: error.message,
      timestamp: Date.now(),
      code: error.code || 'UNKNOWN',
    });
    return newErrors;
  });
}

// エラーをクリアする関数
export function clearError(key) {
  errors.update((errorMap) => {
    const newErrors = new Map(errorMap);
    newErrors.delete(key);
    return newErrors;
  });
}

// エラーを自動的にクリアする関数(一定時間後)
export function setTemporaryError(
  key,
  error,
  duration = 5000
) {
  setError(key, error);
  setTimeout(() => {
    clearError(key);
  }, duration);
}

実践的な API 通信パターン

カスタムフックの作成

Svelte ではカスタムフックの概念はありませんが、同様の機能を実現するパターンがあります。

API 通信用のカスタム関数

javascript// utils/apiHooks.js
import { writable } from 'svelte/store';

// 汎用的なAPI通信フック
export function createApiHook(apiFunction) {
  const data = writable(null);
  const loading = writable(false);
  const error = writable(null);

  async function execute(...args) {
    loading.set(true);
    error.set(null);

    try {
      const result = await apiFunction(...args);
      data.set(result);
      return result;
    } catch (err) {
      error.set(err.message);
      throw err;
    } finally {
      loading.set(false);
    }
  }

  return {
    data,
    loading,
    error,
    execute,
  };
}

// 使用例
export const useUsers = createApiHook(async () => {
  const response = await fetch('/api/users');
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
  return await response.json();
});

再利用可能な API 関数の設計

再利用可能で保守しやすい API 関数を設計することで、開発効率を大幅に向上させることができます。

API クライアントクラスの実装

javascript// services/ApiClient.js
class ApiClient {
  constructor(baseURL = '', defaultHeaders = {}) {
    this.baseURL = baseURL;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
      ...defaultHeaders,
    };
  }

  // 認証トークンを設定
  setAuthToken(token) {
    this.defaultHeaders[
      'Authorization'
    ] = `Bearer ${token}`;
  }

  // 汎用的なリクエストメソッド
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      headers: {
        ...this.defaultHeaders,
        ...options.headers,
      },
      ...options,
    };

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

      if (!response.ok) {
        const errorData = await this.parseErrorResponse(
          response
        );
        throw new Error(
          errorData.message || `HTTP ${response.status}`
        );
      }

      return await this.parseResponse(response);
    } catch (error) {
      this.handleError(error);
      throw error;
    }
  }

  // GETリクエスト
  async get(endpoint, params = {}) {
    const queryString = new URLSearchParams(
      params
    ).toString();
    const url = queryString
      ? `${endpoint}?${queryString}`
      : endpoint;

    return this.request(url, { method: 'GET' });
  }

  // POSTリクエスト
  async post(endpoint, data = {}) {
    return this.request(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  // PUTリクエスト
  async put(endpoint, data = {}) {
    return this.request(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  // DELETEリクエスト
  async delete(endpoint) {
    return this.request(endpoint, { method: 'DELETE' });
  }

  // レスポンスの解析
  async parseResponse(response) {
    const contentType =
      response.headers.get('content-type');
    if (
      contentType &&
      contentType.includes('application/json')
    ) {
      return await response.json();
    }
    return await response.text();
  }

  // エラーレスポンスの解析
  async parseErrorResponse(response) {
    try {
      return await response.json();
    } catch {
      return {
        message: `HTTP ${response.status}: ${response.statusText}`,
      };
    }
  }

  // エラーハンドリング
  handleError(error) {
    console.error('API Error:', error);

    // ネットワークエラーの処理
    if (error.name === 'TypeError') {
      throw new Error(
        'ネットワーク接続エラー: インターネット接続を確認してください'
      );
    }

    // 認証エラーの処理
    if (error.message.includes('401')) {
      // 認証トークンの再取得やログイン画面へのリダイレクト
      console.warn('認証エラーが発生しました');
    }
  }
}

// インスタンスの作成
export const apiClient = new ApiClient('/api');

型安全性の確保

TypeScript を使用することで、API 通信における型安全性を確保できます。

型定義の実装

typescript// types/api.ts
export interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

export interface CreateUserRequest {
  name: string;
  email: string;
  password: string;
}

export interface UpdateUserRequest {
  name?: string;
  email?: string;
}

export interface ApiResponse<T> {
  data: T;
  message?: string;
  success: boolean;
}

export interface PaginatedResponse<T> {
  data: T[];
  pagination: {
    page: number;
    limit: number;
    total: number;
    totalPages: number;
  };
}

// エラーの型定義
export interface ApiError {
  message: string;
  code: string;
  status: number;
}

型安全な API 関数

typescript// services/userService.ts
import { apiClient } from './ApiClient';
import type {
  User,
  CreateUserRequest,
  UpdateUserRequest,
  ApiResponse,
  PaginatedResponse,
} from '../types/api';

export class UserService {
  // ユーザー一覧の取得
  static async getUsers(
    page = 1,
    limit = 10
  ): Promise<PaginatedResponse<User>> {
    return apiClient.get('/users', { page, limit });
  }

  // 特定のユーザーの取得
  static async getUser(id: number): Promise<User> {
    return apiClient.get(`/users/${id}`);
  }

  // ユーザーの作成
  static async createUser(
    userData: CreateUserRequest
  ): Promise<User> {
    return apiClient.post('/users', userData);
  }

  // ユーザーの更新
  static async updateUser(
    id: number,
    userData: UpdateUserRequest
  ): Promise<User> {
    return apiClient.put(`/users/${id}`, userData);
  }

  // ユーザーの削除
  static async deleteUser(id: number): Promise<void> {
    return apiClient.delete(`/users/${id}`);
  }
}

パフォーマンス最適化

キャッシュ戦略

API 通信のパフォーマンスを向上させるため、適切なキャッシュ戦略を実装しましょう。

シンプルなキャッシュシステム

javascript// utils/cache.js
class ApiCache {
  constructor() {
    this.cache = new Map();
    this.defaultTTL = 5 * 60 * 1000; // 5分
  }

  // キャッシュにデータを保存
  set(key, data, ttl = this.defaultTTL) {
    const expiry = Date.now() + ttl;
    this.cache.set(key, {
      data,
      expiry,
    });
  }

  // キャッシュからデータを取得
  get(key) {
    const item = this.cache.get(key);

    if (!item) {
      return null;
    }

    if (Date.now() > item.expiry) {
      this.cache.delete(key);
      return null;
    }

    return item.data;
  }

  // キャッシュをクリア
  clear() {
    this.cache.clear();
  }

  // 特定のキーのキャッシュを削除
  delete(key) {
    this.cache.delete(key);
  }

  // 期限切れのキャッシュを削除
  cleanup() {
    const now = Date.now();
    for (const [key, item] of this.cache.entries()) {
      if (now > item.expiry) {
        this.cache.delete(key);
      }
    }
  }
}

export const apiCache = new ApiCache();

キャッシュ付き API 関数

javascript// キャッシュを活用したAPI呼び出し
async function fetchWithCache(url, options = {}) {
  const cacheKey = `${url}-${JSON.stringify(options)}`;

  // キャッシュから取得を試行
  const cachedData = apiCache.get(cacheKey);
  if (cachedData) {
    return cachedData;
  }

  // APIからデータを取得
  const response = await fetch(url, options);
  if (!response.ok) {
    throw new Error(
      `HTTP ${response.status}: ${response.statusText}`
    );
  }

  const data = await response.json();

  // キャッシュに保存
  apiCache.set(cacheKey, data);

  return data;
}

リクエストの最適化

不要なリクエストを防ぎ、効率的な API 通信を実現するためのテクニックを学びましょう。

デバウンス機能の実装

javascript// utils/debounce.js
export function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// 検索機能でのデバウンス使用例
const debouncedSearch = debounce(async (query) => {
  if (query.length < 2) return;

  try {
    const response = await fetch(
      `/api/search?q=${encodeURIComponent(query)}`
    );
    const results = await response.json();
    searchResults.set(results);
  } catch (error) {
    console.error('検索エラー:', error);
  }
}, 300);

リクエストの重複防止

javascript// utils/requestManager.js
class RequestManager {
  constructor() {
    this.pendingRequests = new Map();
  }

  // 重複リクエストを防ぐ関数
  async execute(key, requestFunction) {
    // 既に同じリクエストが進行中の場合は、そのPromiseを返す
    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key);
    }

    // 新しいリクエストを作成
    const promise = requestFunction().finally(() => {
      this.pendingRequests.delete(key);
    });

    this.pendingRequests.set(key, promise);
    return promise;
  }

  // 特定のリクエストをキャンセル
  cancel(key) {
    if (this.pendingRequests.has(key)) {
      this.pendingRequests.delete(key);
    }
  }

  // すべてのリクエストをキャンセル
  cancelAll() {
    this.pendingRequests.clear();
  }
}

export const requestManager = new RequestManager();

メモリリークの防止

Svelte コンポーネントでのメモリリークを防ぐためのベストプラクティスを実装しましょう。

コンポーネントでの適切なクリーンアップ

javascript<!-- UserProfile.svelte -->
<script>
    import { onMount, onDestroy } from 'svelte';
    import { users, fetchUsers } from './stores/api.js';

    let abortController = null;

    onMount(() => {
        // AbortControllerを使用してリクエストをキャンセル可能にする
        abortController = new AbortController();
        loadUserData();
    });

    onDestroy(() => {
        // コンポーネントが破棄される際にリクエストをキャンセル
        if (abortController) {
            abortController.abort();
        }
    });

    async function loadUserData() {
        try {
            const response = await fetch('/api/user/profile', {
                signal: abortController.signal
            });

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

            const userData = await response.json();
            users.set(userData);
        } catch (error) {
            if (error.name === 'AbortError') {
                console.log('リクエストがキャンセルされました');
                return;
            }
            console.error('ユーザーデータ取得エラー:', error);
        }
    }
</script>

<div class="user-profile">
    {#if $users}
        <h2>{$users.name}</h2>
        <p>{$users.email}</p>
    {/if}
</div>

ストアのクリーンアップ

javascript// stores/cleanup.js
import { get } from 'svelte/store';

// ストアのクリーンアップ関数
export function cleanupStores() {
  // すべてのストアをリセット
  users.set([]);
  usersLoading.set(false);
  usersError.set(null);

  // キャッシュをクリア
  apiCache.clear();

  // 進行中のリクエストをキャンセル
  requestManager.cancelAll();
}

// ページ離脱時のクリーンアップ
window.addEventListener('beforeunload', () => {
  cleanupStores();
});

まとめ

Svelte での API 通信について、fetch API を中心とした実践的なテクニックを詳しく解説してきました。

学んだことの振り返り

  1. Svelte のリアクティブシステムを活用した効率的なデータ管理
  2. fetch API の基本操作とエラーハンドリングの重要性
  3. ストアを使った状態管理による保守性の向上
  4. 再利用可能な API 関数の設計による開発効率の向上
  5. パフォーマンス最適化によるユーザーエクスペリエンスの向上

心に響くポイント

Svelte での API 通信の魅力は、そのシンプルさと直感性にあります。複雑な設定やボイラープレートコードが少なく、本質的な機能に集中できる点が大きな魅力です。

また、ブラウザ標準の fetch API を使用することで、追加のライブラリに依存せずに強力な API 通信機能を実現できることも重要なポイントです。これにより、バンドルサイズを抑えながら、高速で効率的なアプリケーションを構築できます。

次のステップの提案

  1. TypeScript の導入: 型安全性を向上させ、開発時のエラーを減らしましょう
  2. テストの実装: API 通信のテストを書くことで、品質を向上させましょう
  3. エラーモニタリング: 本番環境でのエラー監視システムを構築しましょう
  4. パフォーマンス監視: 実際のユーザー体験を測定し、継続的に改善しましょう

Svelte での API 通信は、学べば学ぶほどその奥深さと可能性を感じることができます。この記事で学んだ知識を基に、より良いユーザーエクスペリエンスを提供するアプリケーションを構築してください。

関連リンク