T-CREATOR

Tauri と API 通信:バックエンド連携の基本

Tauri と API 通信:バックエンド連携の基本

Tauri アプリケーションでバックエンドと連携する際、適切な API 通信の実装は欠かせません。デスクトップアプリケーションでありながら Web 技術を活用する Tauri では、従来の Web アプリケーションとは異なる考慮点があります。

本記事では、Tauri アプリケーションにおける基本的な API 通信から、セキュリティを考慮した実装方法まで、段階的に解説いたします。実際のコード例を交えながら、実用的な知識を身につけていただけるでしょう。

背景

Tauri アプリケーションにおけるデータ通信の必要性

現代のアプリケーション開発では、フロントエンドとバックエンドの分離が当たり前となっています。Tauri アプリケーションも例外ではありません。

ユーザーデータの永続化、外部サービスとの連携、リアルタイム更新など、さまざまな場面でバックエンドとの通信が必要になります。特に SaaS 型のアプリケーションでは、複数のデバイス間でのデータ同期が重要な要素となります。

以下の図は、Tauri アプリケーションの基本的なアーキテクチャを示しています。

mermaidflowchart LR
    user[ユーザー] -->|操作| tauri[Tauriアプリ]
    tauri -->|HTTP/WebSocket| api[Backend API]
    api -->|クエリ| db[(データベース)]
    api -->|レスポンス| tauri
    tauri -->|UI更新| user

    subgraph "Tauriアプリ内部"
        frontend[フロントエンド<br/>React/Vue/Svelte]
        rust[Rustバックエンド<br/>tauri::command]
        frontend <--> rust
    end

    tauri --- frontend
    tauri --- rust

補足: Tauri アプリ内部では、フロントエンドと Rust バックエンドが密接に連携し、外部 API との通信を効率的に処理します。

フロントエンドとバックエンドの分離のメリット

アーキテクチャの分離により、以下のような利点が得られます。

スケーラビリティの向上 バックエンドを独立させることで、複数のクライアントアプリケーションから同一の API を利用できます。将来的にモバイルアプリや Web アプリを追加する際も、既存のバックエンドを活用できるでしょう。

開発効率の改善 フロントエンドとバックエンドの開発チームが並行して作業できるため、開発スピードが向上します。また、それぞれの技術領域に特化した最適な技術選択が可能になります。

セキュリティの強化 認証やデータ検証をバックエンドで一元管理することで、セキュリティホールを減らせます。機密情報はサーバー側で管理し、クライアント側には必要最小限の情報のみを送信できるのです。

Web 技術とネイティブアプリの橋渡し

Tauri は Web 技術を使いながら、ネイティブアプリケーションの恩恵を受けられる独特なプラットフォームです。

この特性を活かすため、以下の点を考慮した設計が重要になります。

項目Web 技術の活用ネイティブ機能の活用
UI/UXHTML/CSS/JavaScript による柔軟な画面構築OS ネイティブなウィンドウ機能、メニュー
データ処理フロントエンドでのリアクティブな状態管理Rust による高速なデータ処理、ファイルシステムアクセス
通信Fetch API による標準的な HTTP 通信システムレベルのネットワーク制御
セキュリティHTTPS、CSP による Web 標準のセキュリティOS レベルのセキュリティ機能活用

図で理解できる要点:

  • Tauri アプリは内部でフロントエンドと Rust バックエンドが連携している
  • 外部 API との通信は効率的に処理される仕組みがある
  • アーキテクチャの分離により拡張性とセキュリティが向上する

課題

従来の Electron との違い

Tauri と Electron では、API 通信において重要な違いがあります。まず理解しておくべきポイントを整理しましょう。

アーキテクチャの根本的差異 Electron が Node.js ベースであるのに対し、Tauri は Rust をコアとしています。これにより、パフォーマンス面では大幅な向上が期待できる一方、Node.js エコシステムの豊富な HTTP クライアントライブラリを直接利用できません。

セキュリティモデルの違い Electron では比較的自由度が高い一方、Tauri はより厳格なセキュリティモデルを採用しています。すべての API アクセスは明示的な許可が必要で、設定漏れがあると通信エラーが発生します。

以下の図は、Electron と Tauri のアーキテクチャの違いを示しています。

mermaidflowchart TB
    subgraph "Electron アーキテクチャ"
        e_ui[Webページ] --> e_main[メインプロセス<br/>Node.js]
        e_main --> e_api[外部API]
        e_ui --> e_api
    end

    subgraph "Tauri アーキテクチャ"
        t_ui[Webページ] --> t_core[Tauriコア<br/>Rust]
        t_core --> t_api[外部API]
        t_ui -.->|直接通信<br/>制限あり| t_api
    end

補足: Tauri では直接的な API 通信に制限があり、多くの場合 Rust コアを経由した通信が推奨されます。

セキュリティ制約下での API 通信

Tauri アプリケーションでは、以下のセキュリティ制約があります。

Content Security Policy (CSP) の厳格な適用 デフォルトでインラインスクリプトや外部リソースの読み込みが制限されています。API 通信時も、許可されたドメインとのみ通信可能です。

javascript// CSPにより以下のようなコードは動作しない可能性があります
// eval()、innerHTML、document.write() など

タウリ設定ファイルでの明示的許可 tauri.conf.json で通信先を明示的に許可する必要があります。

json{
  "tauri": {
    "allowlist": {
      "http": {
        "all": false,
        "request": true,
        "scope": ["https://api.example.com/*"]
      }
    }
  }
}

この設定により、指定されたスコープ外の API へのアクセスがブロックされます。開発時は問題なく動作していても、本番環境で通信エラーが発生することがあるため注意が必要です。

証明書検証の強化 自己署名証明書や無効な証明書を持つ API との通信は、デフォルトで拒否されます。開発環境でテスト用の API サーバーを使用する際は、適切な証明書の設定が必要です。

パフォーマンスと安定性の両立

デスクトップアプリケーションとして、Web アプリケーションとは異なるパフォーマンス要件があります。

メモリ使用量の最適化 大量のデータを扱う API 通信では、メモリリークや過度なメモリ使用に注意が必要です。特にリアルタイム通信や定期的なポーリングでは、適切なリソース管理が重要になります。

ネットワーク断線への対応 デスクトップアプリケーションは、Web アプリケーションよりも長時間稼働する傾向があります。ネットワークの一時的な断線や接続品質の変化に対する適切な処理が必要です。

UI の応答性維持 同期的な API 通信により、UI がフリーズすることは避けなければなりません。すべての通信は非同期で実装し、ローディング状態を適切に表示する必要があります。

図で理解できる要点:

  • Electron と Tauri ではセキュリティモデルが根本的に異なる
  • Tauri は厳格な CSP とスコープ制限を適用している
  • パフォーマンスと安定性の両立には特別な配慮が必要

解決策

Tauri のfetchを使った HTTP 通信

Tauri での HTTP 通信の基本的なアプローチから始めましょう。フロントエンド側では標準的な Fetch API を使用できますが、設定に注意が必要です。

基本的な GET リクエストの実装

まず、シンプルな GET リクエストの実装例をご紹介します。

javascript// API通信用のユーティリティ関数
async function fetchData(url, options = {}) {
  try {
    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
      },
      ...options,
    });

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

    return await response.json();
  } catch (error) {
    console.error('API通信エラー:', error);
    throw error;
  }
}

このコードでは、エラーハンドリングとレスポンス検証を含めた基本的な HTTP 通信を実装しています。

POST リクエストでのデータ送信

次に、データを送信する POST リクエストの実装を見てみましょう。

javascript// ユーザーデータを送信する関数
async function createUser(userData) {
  const response = await fetch(
    'https://api.example.com/users',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(userData),
    }
  );

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

  return await response.json();
}

POST リクエストでは、body パラメータを使って JSON データを送信します。エラー処理では、サーバーからのエラーメッセージも取得できるよう実装しています。

設定ファイルでの通信許可

Fetch API を使用するためには、tauri.conf.json での設定が必要です。

json{
  "tauri": {
    "allowlist": {
      "http": {
        "all": false,
        "request": true,
        "scope": [
          "https://api.example.com/*",
          "https://auth.example.com/*"
        ]
      }
    }
  }
}

この設定により、指定されたドメインとの HTTP 通信が許可されます。

tauri::commandを活用した Rust 側 API 連携

より高度な制御やパフォーマンスが必要な場合、Rust 側で API 通信を実装する方法があります。

Rust での基本的な HTTP クライアント実装

まず、必要な依存関係を Cargo.toml に追加します。

toml[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

次に、Rust でのコマンド関数を実装します。

rustuse serde::{Deserialize, Serialize};
use reqwest;

#[derive(Serialize, Deserialize)]
pub struct User {
    pub id: Option<u32>,
    pub name: String,
    pub email: String,
}

#[tauri::command]
async fn fetch_users() -> Result<Vec<User>, String> {
    let client = reqwest::Client::new();

    let response = client
        .get("https://api.example.com/users")
        .header("Content-Type", "application/json")
        .send()
        .await
        .map_err(|e| format!("リクエストエラー: {}", e))?;

    if response.status().is_success() {
        let users: Vec<User> = response
            .json()
            .await
            .map_err(|e| format!("JSON解析エラー: {}", e))?;

        Ok(users)
    } else {
        Err(format!("APIエラー: {}", response.status()))
    }
}

このコマンド関数は、非同期でユーザー一覧を取得し、エラーハンドリングも含んでいます。

フロントエンドからの Rust コマンド呼び出し

フロントエンド側では、@tauri-apps​/​api を使って Rust 関数を呼び出します。

javascriptimport { invoke } from '@tauri-apps/api/tauri';

async function loadUsers() {
  try {
    const users = await invoke('fetch_users');
    console.log('取得したユーザー:', users);
    return users;
  } catch (error) {
    console.error('ユーザー取得エラー:', error);
    throw error;
  }
}

コマンドの登録

main.rs でコマンドを登録する必要があります。

rustfn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![fetch_users])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

エラーハンドリングとセキュリティ対策

堅牢なアプリケーションのためのエラーハンドリング戦略を確立しましょう。

包括的なエラーハンドリングシステム

以下のようなエラー処理システムを実装することで、ユーザー体験を向上できます。

javascript// エラータイプの定義
const ERROR_TYPES = {
  NETWORK_ERROR: 'NETWORK_ERROR',
  API_ERROR: 'API_ERROR',
  AUTH_ERROR: 'AUTH_ERROR',
  VALIDATION_ERROR: 'VALIDATION_ERROR',
};

// エラーハンドリング用のユーティリティクラス
class ApiErrorHandler {
  static handleError(error, context = '') {
    console.error(
      `[${context}] エラーが発生しました:`,
      error
    );

    if (
      error.name === 'TypeError' &&
      error.message.includes('fetch')
    ) {
      return {
        type: ERROR_TYPES.NETWORK_ERROR,
        message: 'ネットワーク接続を確認してください',
        canRetry: true,
      };
    }

    if (error.message.includes('401')) {
      return {
        type: ERROR_TYPES.AUTH_ERROR,
        message: '認証が必要です',
        canRetry: false,
      };
    }

    return {
      type: ERROR_TYPES.API_ERROR,
      message:
        error.message || '予期しないエラーが発生しました',
      canRetry: true,
    };
  }
}

セキュリティ強化のためのトークン管理

認証トークンの安全な管理を実装します。

javascriptimport { invoke } from '@tauri-apps/api/tauri';

class SecureTokenManager {
  static async storeToken(token) {
    // Rust側のセキュアストレージに保存
    return await invoke('store_secure_token', { token });
  }

  static async getToken() {
    return await invoke('get_secure_token');
  }

  static async clearToken() {
    return await invoke('clear_secure_token');
  }
}

// 認証付きAPI通信の実装
async function authenticatedFetch(url, options = {}) {
  const token = await SecureTokenManager.getToken();

  if (!token) {
    throw new Error('認証トークンが見つかりません');
  }

  return fetch(url, {
    ...options,
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });
}

対応する Rust 側の実装も重要です。

rustuse keyring::Entry;

#[tauri::command]
async fn store_secure_token(token: String) -> Result<(), String> {
    let entry = Entry::new("myapp", "auth_token")
        .map_err(|e| format!("キーリング作成エラー: {}", e))?;

    entry.set_password(&token)
        .map_err(|e| format!("トークン保存エラー: {}", e))?;

    Ok(())
}

#[tauri::command]
async fn get_secure_token() -> Result<String, String> {
    let entry = Entry::new("myapp", "auth_token")
        .map_err(|e| format!("キーリング作成エラー: {}", e))?;

    entry.get_password()
        .map_err(|e| format!("トークン取得エラー: {}", e))
}

図で理解できる要点:

  • Fetch API とtauri::commandの両方を使い分けることで柔軟な実装が可能
  • エラーハンドリングは包括的なシステムとして設計する
  • セキュリティ対策には OS レベルのセキュアストレージを活用する

具体例

REST API との基本的な通信実装

実際の Web アプリケーションでよく使われる CRUD 操作を実装してみましょう。タスク管理アプリケーションを例に、基本的な REST API 通信を解説します。

データモデルの定義

まず、タスクデータの型定義から始めます。

typescript// types/task.ts
export interface Task {
  id?: number;
  title: string;
  description: string;
  completed: boolean;
  createdAt?: string;
  updatedAt?: string;
}

export interface CreateTaskRequest {
  title: string;
  description: string;
}

export interface UpdateTaskRequest {
  title?: string;
  description?: string;
  completed?: boolean;
}

型定義により、開発時の型安全性が向上し、バグの早期発見につながります。

API 通信クラスの実装

次に、タスク API との通信を担当するクラスを実装します。

typescript// api/taskApi.ts
import {
  Task,
  CreateTaskRequest,
  UpdateTaskRequest,
} from '../types/task';

class TaskApi {
  private baseUrl = 'https://api.example.com/tasks';

  // 全タスクの取得
  async getAllTasks(): Promise<Task[]> {
    const response = await fetch(this.baseUrl);

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

    return await response.json();
  }

  // タスクの作成
  async createTask(
    taskData: CreateTaskRequest
  ): Promise<Task> {
    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(taskData),
    });

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

    return await response.json();
  }

  // タスクの更新
  async updateTask(
    id: number,
    updates: UpdateTaskRequest
  ): Promise<Task> {
    const response = await fetch(`${this.baseUrl}/${id}`, {
      method: 'PUT',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(updates),
    });

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

    return await response.json();
  }

  // タスクの削除
  async deleteTask(id: number): Promise<void> {
    const response = await fetch(`${this.baseUrl}/${id}`, {
      method: 'DELETE',
    });

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

export const taskApi = new TaskApi();

このクラスでは、基本的な CRUD 操作を実装し、エラーハンドリングも含めています。

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

フロントエンド側で API を使用する実装例をご紹介します。

typescript// components/TaskManager.tsx
import React, { useState, useEffect } from 'react';
import { Task } from '../types/task';
import { taskApi } from '../api/taskApi';

export const TaskManager: React.FC = () => {
  const [tasks, setTasks] = useState<Task[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // タスクの読み込み
  const loadTasks = async () => {
    setLoading(true);
    setError(null);

    try {
      const taskList = await taskApi.getAllTasks();
      setTasks(taskList);
    } catch (err) {
      setError(
        err instanceof Error ? err.message : '不明なエラー'
      );
    } finally {
      setLoading(false);
    }
  };

  // コンポーネント初期化時にタスクを読み込み
  useEffect(() => {
    loadTasks();
  }, []);

  // 新しいタスクの作成
  const handleCreateTask = async (
    title: string,
    description: string
  ) => {
    try {
      const newTask = await taskApi.createTask({
        title,
        description,
      });
      setTasks((prev) => [...prev, newTask]);
    } catch (err) {
      setError(
        err instanceof Error
          ? err.message
          : 'タスク作成エラー'
      );
    }
  };

  return (
    <div>
      {loading && <p>読み込み中...</p>}
      {error && <p>エラー: {error}</p>}

      <div>
        {tasks.map((task) => (
          <div key={task.id}>
            <h3>{task.title}</h3>
            <p>{task.description}</p>
            <p>
              完了: {task.completed ? 'はい' : 'いいえ'}
            </p>
          </div>
        ))}
      </div>
    </div>
  );
};

この実装では、ローディング状態とエラー状態を適切に管理し、ユーザー体験を向上させています。

JSON データの送受信

複雑なデータ構造の送受信について、詳しく見てみましょう。

ネストした JSON データの処理

以下のような複雑なデータ構造を扱うケースを考えてみます。

typescript// types/project.ts
export interface Project {
  id?: number;
  name: string;
  description: string;
  tasks: Task[];
  members: Member[];
  settings: ProjectSettings;
}

interface Member {
  id: number;
  name: string;
  role: 'admin' | 'member' | 'viewer';
}

interface ProjectSettings {
  isPublic: boolean;
  notifications: {
    email: boolean;
    push: boolean;
  };
  deadlines: {
    enabled: boolean;
    defaultDays: number;
  };
}

データ送信時のバリデーション

送信前のデータ検証を実装することで、API エラーを削減できます。

typescript// utils/validator.ts
export class DataValidator {
  static validateProject(
    project: Partial<Project>
  ): string[] {
    const errors: string[] = [];

    if (!project.name || project.name.trim().length === 0) {
      errors.push('プロジェクト名は必須です');
    }

    if (project.name && project.name.length > 100) {
      errors.push(
        'プロジェクト名は100文字以内で入力してください'
      );
    }

    if (!project.description) {
      errors.push('プロジェクトの説明は必須です');
    }

    return errors;
  }

  static validateEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

プロジェクト管理 API の実装

バリデーション機能を含む API クラスを実装します。

typescript// api/projectApi.ts
import { Project } from '../types/project';
import { DataValidator } from '../utils/validator';

class ProjectApi {
  private baseUrl = 'https://api.example.com/projects';

  async createProject(
    projectData: Partial<Project>
  ): Promise<Project> {
    // バリデーション実行
    const validationErrors =
      DataValidator.validateProject(projectData);
    if (validationErrors.length > 0) {
      throw new Error(
        `入力エラー: ${validationErrors.join(', ')}`
      );
    }

    const response = await fetch(this.baseUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(projectData),
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(
        errorData.message ||
          'プロジェクト作成に失敗しました'
      );
    }

    return await response.json();
  }
}

認証機能付き API 通信

セキュアな API 通信を実現するため、JWT 認証を実装してみましょう。

認証状態管理の実装

認証情報とトークンの管理を行うクラスを作成します。

typescript// auth/AuthManager.ts
import { invoke } from '@tauri-apps/api/tauri';

export interface AuthState {
  isAuthenticated: boolean;
  user?: {
    id: number;
    name: string;
    email: string;
  };
  token?: string;
}

class AuthManager {
  private static instance: AuthManager;
  private authState: AuthState = { isAuthenticated: false };
  private listeners: ((state: AuthState) => void)[] = [];

  static getInstance(): AuthManager {
    if (!AuthManager.instance) {
      AuthManager.instance = new AuthManager();
    }
    return AuthManager.instance;
  }

  // ログイン処理
  async login(
    email: string,
    password: string
  ): Promise<void> {
    const response = await fetch(
      'https://api.example.com/auth/login',
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email, password }),
      }
    );

    if (!response.ok) {
      throw new Error('ログインに失敗しました');
    }

    const authData = await response.json();

    // セキュアストレージにトークンを保存
    await invoke('store_secure_token', {
      token: authData.token,
    });

    this.authState = {
      isAuthenticated: true,
      user: authData.user,
      token: authData.token,
    };

    this.notifyListeners();
  }

  // 認証状態の変更を通知
  private notifyListeners() {
    this.listeners.forEach((listener) =>
      listener(this.authState)
    );
  }

  // 認証状態の変更を監視
  onAuthStateChanged(callback: (state: AuthState) => void) {
    this.listeners.push(callback);

    // 現在の状態を即座に通知
    callback(this.authState);

    // アンサブスクライブ用の関数を返す
    return () => {
      this.listeners = this.listeners.filter(
        (l) => l !== callback
      );
    };
  }
}

export const authManager = AuthManager.getInstance();

認証が必要な API の実装

すべての API 呼び出しで自動的に認証ヘッダーを付与する実装です。

typescript// api/authenticatedApi.ts
import { authManager } from '../auth/AuthManager';

class AuthenticatedApi {
  private baseUrl = 'https://api.example.com';

  private async getAuthHeaders(): Promise<HeadersInit> {
    const token = authManager.getToken();

    if (!token) {
      throw new Error('認証が必要です');
    }

    return {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    };
  }

  async get(endpoint: string): Promise<any> {
    const headers = await this.getAuthHeaders();

    const response = await fetch(
      `${this.baseUrl}${endpoint}`,
      {
        method: 'GET',
        headers,
      }
    );

    return this.handleResponse(response);
  }

  async post(endpoint: string, data: any): Promise<any> {
    const headers = await this.getAuthHeaders();

    const response = await fetch(
      `${this.baseUrl}${endpoint}`,
      {
        method: 'POST',
        headers,
        body: JSON.stringify(data),
      }
    );

    return this.handleResponse(response);
  }

  private async handleResponse(response: Response) {
    if (response.status === 401) {
      // 認証エラーの場合、ログアウト処理を実行
      await authManager.logout();
      throw new Error(
        '認証が無効です。再度ログインしてください。'
      );
    }

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

    return await response.json();
  }
}

export const authenticatedApi = new AuthenticatedApi();

WebSocket を使ったリアルタイム通信

リアルタイム通信が必要なアプリケーションでは、WebSocket を活用できます。

WebSocket 接続管理クラス

以下は、WebSocket 接続を管理するクラスの実装例です。

typescript// websocket/WebSocketManager.ts
class WebSocketManager {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;
  private reconnectInterval = 1000;
  private messageHandlers = new Map<
    string,
    (data: any) => void
  >();

  async connect(url: string): Promise<void> {
    return new Promise((resolve, reject) => {
      this.ws = new WebSocket(url);

      this.ws.onopen = () => {
        console.log('WebSocket 接続完了');
        this.reconnectAttempts = 0;
        resolve();
      };

      this.ws.onmessage = (event) => {
        try {
          const message = JSON.parse(event.data);
          this.handleMessage(message);
        } catch (error) {
          console.error('メッセージ解析エラー:', error);
        }
      };

      this.ws.onclose = () => {
        console.log('WebSocket 接続切断');
        this.attemptReconnect(url);
      };

      this.ws.onerror = (error) => {
        console.error('WebSocket エラー:', error);
        reject(error);
      };
    });
  }

  private attemptReconnect(url: string) {
    if (
      this.reconnectAttempts < this.maxReconnectAttempts
    ) {
      this.reconnectAttempts++;
      console.log(
        `再接続試行中 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`
      );

      setTimeout(() => {
        this.connect(url);
      }, this.reconnectInterval * this.reconnectAttempts);
    } else {
      console.error('再接続試行回数の上限に達しました');
    }
  }

  // メッセージハンドラーの登録
  onMessage(type: string, handler: (data: any) => void) {
    this.messageHandlers.set(type, handler);
  }

  private handleMessage(message: {
    type: string;
    data: any;
  }) {
    const handler = this.messageHandlers.get(message.type);
    if (handler) {
      handler(message.data);
    }
  }

  // メッセージの送信
  send(type: string, data: any) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, data }));
    } else {
      console.error('WebSocket接続が確立されていません');
    }
  }
}

export const wsManager = new WebSocketManager();

リアルタイム通知システムの実装

WebSocket を使った通知システムの実装例です。

typescript// components/NotificationManager.tsx
import React, { useEffect, useState } from 'react';
import { wsManager } from '../websocket/WebSocketManager';

interface Notification {
  id: string;
  type: 'info' | 'success' | 'warning' | 'error';
  message: string;
  timestamp: Date;
}

export const NotificationManager: React.FC = () => {
  const [notifications, setNotifications] = useState<
    Notification[]
  >([]);

  useEffect(() => {
    // WebSocket接続の初期化
    wsManager.connect(
      'wss://api.example.com/notifications'
    );

    // 通知メッセージのハンドラー登録
    wsManager.onMessage('notification', (data) => {
      const newNotification: Notification = {
        ...data,
        timestamp: new Date(),
      };

      setNotifications((prev) => [
        newNotification,
        ...prev,
      ]);

      // 5秒後に通知を自動削除
      setTimeout(() => {
        setNotifications((prev) =>
          prev.filter((n) => n.id !== newNotification.id)
        );
      }, 5000);
    });

    return () => {
      // クリーンアップ処理
    };
  }, []);

  return (
    <div className='notification-container'>
      {notifications.map((notification) => (
        <div
          key={notification.id}
          className={`notification ${notification.type}`}
        >
          <p>{notification.message}</p>
          <small>
            {notification.timestamp.toLocaleTimeString()}
          </small>
        </div>
      ))}
    </div>
  );
};

以下の図は、WebSocket を使ったリアルタイム通信のフローを示しています。

mermaidsequenceDiagram
    participant Client as Tauriアプリ
    participant WS as WebSocketサーバー
    participant API as バックエンドAPI
    participant DB as データベース

    Client->>WS: WebSocket接続
    WS-->>Client: 接続完了

    Note over API, DB: 他のユーザーがデータ更新
    API->>DB: データ更新
    API->>WS: 通知送信
    WS->>Client: リアルタイム通知

    Client->>API: データ取得リクエスト
    API->>DB: クエリ実行
    DB-->>API: 最新データ
    API-->>Client: データレスポンス

補足: WebSocket 接続により、他のユーザーの操作がリアルタイムで反映され、常に最新の状態が保たれます。

図で理解できる要点:

  • REST API では基本的な CRUD 操作を型安全に実装する
  • 認証機能では自動的なトークン管理とエラーハンドリングが重要
  • WebSocket を使うことでリアルタイムな双方向通信が実現できる

まとめ

実装のポイント整理

Tauri アプリケーションでの API 通信実装において、押さえておくべき重要なポイントをまとめます。

設定の重要性 tauri.conf.json での適切な設定は、API 通信の成功に不可欠です。通信先のドメインを事前に許可リストに追加し、必要最小限の権限のみを付与することで、セキュリティとパフォーマンスの両方を確保できます。

エラーハンドリングの充実 ネットワーク環境の変動やサーバー側の問題に対応するため、包括的なエラーハンドリング戦略が必要です。ユーザーフレンドリーなエラーメッセージと、適切なリトライ機能により、優れたユーザー体験を提供できるでしょう。

パフォーマンスの最適化 大量のデータを扱う際は、適切なデータ構造の選択と、メモリ効率を考慮した実装が重要になります。また、必要に応じて Rust 側での処理を活用することで、CPU 集約的な操作のパフォーマンスを大幅に改善できます。

実装レベルフロントエンドRust 側推奨用途
基本Fetch APIなしシンプルな REST API 通信
中級認証付き APIトークン管理セキュアな業務アプリ
上級WebSocket + APIカスタムコマンドリアルタイム + 高パフォーマンス

注意すべき点とベストプラクティス

セキュリティファースト 認証トークンは OS レベルのセキュアストレージに保存し、通信は HTTPS を使用してください。また、API レスポンスに含まれる機密情報の取り扱いには特に注意が必要です。

ユーザー体験の向上 ローディング状態、エラー状態、成功状態を明確に表示することで、ユーザーが現在の状況を理解できるようにしましょう。特に、ネットワーク処理中は UI の応答性を維持することが重要です。

テスタビリティの確保 API 通信のロジックは、テスト可能な形で実装してください。モックサーバーやテスト用の API エンドポイントを用意することで、開発効率が大幅に向上します。

監視と運用 本番環境では、API 通信のエラー率やレスポンス時間を監視し、問題の早期発見に努めましょう。ログの記録も重要ですが、機密情報は記録しないよう注意してください。

Tauri と API 通信の組み合わせにより、Web 技術の利便性とネイティブアプリケーションのパフォーマンスを両立したアプリケーションを構築できます。本記事で紹介した実装パターンを参考に、皆さんのプロジェクトに最適なアーキテクチャを選択していただければと思います。

継続的な学習と実践を通じて、より堅牢で効率的なアプリケーションを開発していけるでしょう。

関連リンク