T-CREATOR

Node.js サードパーティ API との連携パターン

Node.js サードパーティ API との連携パターン

現代の Web アプリケーション開発において、サードパーティ API との連携は避けて通れない重要な技術です。決済処理、ソーシャルログイン、地図表示、メール配信など、多くの機能を外部サービスに依存しているからです。

Node.js での API 連携は、その非同期処理の特性を活かして効率的に実装できますが、同時に適切な設計とエラーハンドリングが求められます。本記事では、実際の開発現場で役立つ連携パターンと、よくある問題への対処法をご紹介いたします。

背景

Node.js でのサードパーティ API 連携の重要性

Node.js は、その軽量性と高いパフォーマンスから、多くの企業で API サーバーやマイクロサービスの開発に採用されています。特に、イベントドリブンなアーキテクチャにより、複数の外部 API を同時に呼び出す処理を効率的に実行できる点が魅力です。

実際の開発現場では、以下のような場面で API 連携が必要になります。

シーン利用する API実装の複雑さ
1決済処理(Stripe、PayPal)
2ソーシャルログイン(Google、Facebook)
3メール配信(SendGrid、Mailgun)
4地図・位置情報(Google Maps)
5画像・動画処理(Cloudinary)

現代の Web アプリケーション開発における API 活用の必要性

マイクロサービスアーキテクチャの普及により、単一のアプリケーションが複数の専門特化したサービスと連携する設計が一般的になりました。これにより、開発チームは自社の核となる機能に集中でき、周辺機能は信頼性の高い外部サービスに委ねることができます。

しかし、この恩恵を受けるためには、堅牢で保守性の高い API 連携の実装が不可欠です。適切に設計されていない API 連携は、アプリケーション全体の安定性を脅かす要因となってしまいます。

課題

API 連携時の共通課題

Node.js で外部 API と連携する際に、多くの開発者が直面する課題があります。これらの課題を事前に理解し、適切な対策を講じることで、安定したアプリケーションを構築できます。

エラーハンドリングの複雑さ

外部 API との通信では、ネットワークエラー、タイムアウト、API 側のエラーレスポンスなど、様々な種類のエラーが発生します。これらを適切に分類し、それぞれに応じた処理を実装する必要があります。

よく遭遇するエラーの例:

javascript// ネットワークエラーの例
Error: connect ECONNREFUSED 127.0.0.1:3000
Error: getaddrinfo ENOTFOUND api.example.com

// タイムアウトエラーの例  
Error: timeout of 5000ms exceeded

// HTTP ステータスエラーの例
Error: Request failed with status code 429 (Too Many Requests)
Error: Request failed with status code 401 (Unauthorized)

認証とセキュリティの管理

API キーや OAuth トークンなどの認証情報を安全に管理し、適切に更新する仕組みが必要です。また、API キーの漏洩や不正使用を防ぐためのセキュリティ対策も重要になります。

レート制限への対応

多くの API にはリクエスト数の制限があり、これを超過すると一時的にサービスが利用できなくなります。適切なレート制限の管理と、制限に達した場合の対処が求められます。

開発者が直面する技術的な問題点

実際の開発現場では、以下のような技術的な課題に直面することが多いです。

非同期処理の複雑化

複数の API を連携させる場合、Promise の連鎖やエラーハンドリングが複雑になりがちです。特に、一つの API の結果を使って別の API を呼び出すような依存関係がある場合は注意が必要です。

デバッグとログの困難さ

外部 API とのやり取りは、ローカル環境での再現が難しく、問題が発生した際のデバッグに時間がかかることがあります。適切なログ設計と監視体制が重要になります。

テストの実装

外部 API に依存するコードのテストは、モックやスタブの実装が必要になり、テストコードが複雑になる傾向があります。

解決策

HTTP クライアントライブラリの選択

Node.js で外部 API と通信する際には、適切な HTTP クライアントライブラリの選択が重要です。それぞれの特徴を理解して、プロジェクトの要件に最適なものを選びましょう。

axios の活用

axios は、Node.js と ブラウザの両方で利用できる人気の HTTP クライアントライブラリです。豊富な機能と直感的な API が特徴で、多くのプロジェクトで採用されています。

以下は、axios を使った基本的な API 呼び出しの実装例です:

javascriptconst axios = require('axios');

// 基本的な GET リクエストの実装
async function fetchUserData(userId) {
  try {
    const response = await axios.get(`https://api.example.com/users/${userId}`, {
      timeout: 5000, // 5秒でタイムアウト
      headers: {
        'Authorization': `Bearer ${process.env.API_TOKEN}`,
        'Content-Type': 'application/json'
      }
    });
    
    return response.data;
  } catch (error) {
    // エラーの種類に応じて適切な処理を実行
    if (error.code === 'ECONNABORTED') {
      throw new Error('API request timeout');
    }
    
    if (error.response?.status === 404) {
      throw new Error('User not found');
    }
    
    throw error;
  }
}

axios の強力な機能の一つがインターセプターです。リクエストやレスポンスの共通処理を定義できます:

javascript// リクエストインターセプターの設定
axios.interceptors.request.use(
  (config) => {
    // 全てのリクエストにタイムスタンプを追加
    config.headers['X-Request-Time'] = Date.now();
    console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// レスポンスインターセプターの設定
axios.interceptors.response.use(
  (response) => {
    // レスポンス時間の計算とログ出力
    const requestTime = response.config.headers['X-Request-Time'];
    const responseTime = Date.now() - requestTime;
    console.log(`API Response: ${response.status} (${responseTime}ms)`);
    return response;
  },
  (error) => {
    // 401エラーの場合は自動的にトークンを更新
    if (error.response?.status === 401) {
      return refreshTokenAndRetry(error.config);
    }
    return Promise.reject(error);
  }
);

fetch API の利用

Node.js 18 以降では、Web 標準の fetch API がネイティブでサポートされています。追加のライブラリを必要とせず、軽量な実装が可能です。

javascript// fetch API を使った実装
async function createUser(userData) {
  try {
    const response = await fetch('https://api.example.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.API_TOKEN}`
      },
      body: JSON.stringify(userData)
    });

    // レスポンスのステータスチェック
    if (!response.ok) {
      const errorBody = await response.text();
      throw new Error(`HTTP ${response.status}: ${errorBody}`);
    }

    return await response.json();
  } catch (error) {
    // ネットワークエラーの処理
    if (error instanceof TypeError && error.message.includes('fetch')) {
      throw new Error('Network connection failed');
    }
    throw error;
  }
}

node-fetch の使い方

Node.js 18 未満の環境や、より高度な機能が必要な場合は、node-fetch ライブラリが便利です:

javascriptconst fetch = require('node-fetch');
const FormData = require('form-data');

// ファイルアップロードの実装
async function uploadImage(imagePath, filename) {
  const form = new FormData();
  const fs = require('fs');
  
  form.append('file', fs.createReadStream(imagePath));
  form.append('filename', filename);

  try {
    const response = await fetch('https://api.example.com/upload', {
      method: 'POST',
      body: form,
      headers: {
        'Authorization': `Bearer ${process.env.API_TOKEN}`,
        ...form.getHeaders() // Content-Type を自動設定
      }
    });

    if (!response.ok) {
      throw new Error(`Upload failed: ${response.statusText}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Upload error:', error.message);
    throw error;
  }
}

認証パターンの実装

外部 API との安全な連携には、適切な認証方式の実装が不可欠です。API の種類や要件に応じて、最適な認証パターンを選択しましょう。

API キー認証

最もシンプルで広く使われている認証方式です。API プロバイダーから発行されたキーをリクエストに含めて認証を行います。

javascriptconst axios = require('axios');

class APIClient {
  constructor(apiKey, baseURL) {
    this.apiKey = apiKey;
    this.client = axios.create({
      baseURL: baseURL,
      timeout: 10000,
      headers: {
        'X-API-Key': apiKey,
        'Content-Type': 'application/json'
      }
    });
    
    // レスポンスエラーの共通処理
    this.client.interceptors.response.use(
      response => response,
      error => this.handleError(error)
    );
  }

  handleError(error) {
    if (error.response?.status === 403) {
      throw new Error('API key is invalid or expired');
    }
    if (error.response?.status === 429) {
      throw new Error('API rate limit exceeded');
    }
    throw error;
  }

  async get(endpoint, params = {}) {
    const response = await this.client.get(endpoint, { params });
    return response.data;
  }
}

// 使用例
const client = new APIClient(process.env.API_KEY, 'https://api.example.com');
const data = await client.get('/users/123');

OAuth 2.0 認証

より高度なセキュリティが求められる場合や、ユーザーの代理でAPI を呼び出す場合に使用される認証方式です:

javascriptconst axios = require('axios');

class OAuthClient {
  constructor(clientId, clientSecret, tokenEndpoint) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.tokenEndpoint = tokenEndpoint;
    this.accessToken = null;
    this.tokenExpiry = null;
  }

  // アクセストークンの取得
  async getAccessToken() {
    if (this.accessToken && this.tokenExpiry > Date.now()) {
      return this.accessToken;
    }

    try {
      const response = await axios.post(this.tokenEndpoint, {
        grant_type: 'client_credentials',
        client_id: this.clientId,
        client_secret: this.clientSecret
      }, {
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
      });

      this.accessToken = response.data.access_token;
      // トークンの有効期限を設定(安全のため10秒早めに期限切れとする)
      this.tokenExpiry = Date.now() + (response.data.expires_in - 10) * 1000;
      
      return this.accessToken;
    } catch (error) {
      throw new Error(`OAuth token acquisition failed: ${error.message}`);
    }
  }

  // 認証付きAPI呼び出し
  async apiCall(url, options = {}) {
    const token = await this.getAccessToken();
    
    return axios({
      url,
      ...options,
      headers: {
        'Authorization': `Bearer ${token}`,
        ...options.headers
      }
    });
  }
}

JWT トークン認証

JSON Web Token(JWT)を使用した認証パターンです。トークンの検証とリフレッシュの実装が重要になります:

javascriptconst jwt = require('jsonwebtoken');
const axios = require('axios');

class JWTClient {
  constructor(secret, refreshEndpoint) {
    this.secret = secret;
    this.refreshEndpoint = refreshEndpoint;
    this.token = null;
  }

  // JWT トークンの検証
  isTokenValid(token) {
    try {
      const decoded = jwt.verify(token, this.secret);
      // 有効期限の確認(5分前にリフレッシュ)
      return decoded.exp > (Date.now() / 1000) + 300;
    } catch (error) {
      return false;
    }
  }

  // トークンのリフレッシュ
  async refreshToken() {
    try {
      const response = await axios.post(this.refreshEndpoint, {
        refresh_token: this.refreshToken
      });
      
      this.token = response.data.access_token;
      return this.token;
    } catch (error) {
      throw new Error(`Token refresh failed: ${error.message}`);
    }
  }

  // 自動的にトークンを管理するAPI呼び出し
  async authenticatedRequest(url, options = {}) {
    if (!this.token || !this.isTokenValid(this.token)) {
      await this.refreshToken();
    }

    return axios({
      url,
      ...options,
      headers: {
        'Authorization': `Bearer ${this.token}`,
        ...options.headers
      }
    });
  }
}

エラーハンドリングとリトライ機構

堅牢な API 連携には、適切なエラーハンドリングとリトライ機構の実装が欠かせません。

エラー処理のベストプラクティス

外部 API のエラーを適切に分類し、それぞれに応じた処理を実装することが重要です:

javascriptclass APIError extends Error {
  constructor(message, status, code, retryable = false) {
    super(message);
    this.name = 'APIError';
    this.status = status;
    this.code = code;
    this.retryable = retryable;
  }
}

class APIErrorHandler {
  static handleError(error) {
    // axios エラーの場合
    if (error.response) {
      const { status, data } = error.response;
      
      switch (status) {
        case 400:
          throw new APIError('Bad Request', status, 'BAD_REQUEST', false);
        case 401:
          throw new APIError('Unauthorized', status, 'UNAUTHORIZED', false);
        case 403:
          throw new APIError('Forbidden', status, 'FORBIDDEN', false);
        case 404:
          throw new APIError('Not Found', status, 'NOT_FOUND', false);
        case 429:
          throw new APIError('Rate Limited', status, 'RATE_LIMITED', true);
        case 500:
        case 502:
        case 503:
        case 504:
          throw new APIError('Server Error', status, 'SERVER_ERROR', true);
        default:
          throw new APIError(`HTTP ${status}`, status, 'UNKNOWN', false);
      }
    }
    
    // ネットワークエラーの場合
    if (error.code) {
      switch (error.code) {
        case 'ECONNABORTED':
          throw new APIError('Request Timeout', 0, 'TIMEOUT', true);
        case 'ECONNREFUSED':
          throw new APIError('Connection Refused', 0, 'CONNECTION_REFUSED', true);
        case 'ENOTFOUND':
          throw new APIError('DNS Resolution Failed', 0, 'DNS_ERROR', true);
        default:
          throw new APIError(`Network Error: ${error.code}`, 0, error.code, true);
      }
    }
    
    throw error;
  }
}

自動リトライの実装

一時的なエラーに対して自動的にリトライを行う機構を実装します:

javascriptclass RetryableAPIClient {
  constructor(baseURL, options = {}) {
    this.client = axios.create({ baseURL });
    this.maxRetries = options.maxRetries || 3;
    this.baseDelay = options.baseDelay || 1000; // 1秒
    this.maxDelay = options.maxDelay || 30000; // 30秒
  }

  // 指数バックオフでのリトライ実装
  async withRetry(requestFn, retries = 0) {
    try {
      return await requestFn();
    } catch (error) {
      const apiError = APIErrorHandler.handleError(error);
      
      // リトライ不可能なエラーまたは最大試行回数を超えた場合
      if (!apiError.retryable || retries >= this.maxRetries) {
        throw apiError;
      }

      // 指数バックオフによる待機時間の計算
      const delay = Math.min(
        this.baseDelay * Math.pow(2, retries),
        this.maxDelay
      );
      
      // ジッターを追加して同時リトライを避ける
      const jitter = delay * 0.1 * Math.random();
      const totalDelay = delay + jitter;

      console.log(`Retrying API call in ${totalDelay}ms (attempt ${retries + 1}/${this.maxRetries})`);
      
      await new Promise(resolve => setTimeout(resolve, totalDelay));
      return this.withRetry(requestFn, retries + 1);
    }
  }

  async get(url, config = {}) {
    return this.withRetry(() => this.client.get(url, config));
  }

  async post(url, data, config = {}) {
    return this.withRetry(() => this.client.post(url, data, config));
  }
}

// 使用例
const client = new RetryableAPIClient('https://api.example.com', {
  maxRetries: 3,
  baseDelay: 1000
});

try {
  const response = await client.get('/users/123');
  console.log(response.data);
} catch (error) {
  console.error('API call failed after retries:', error.message);
}

具体例

REST API との連携実装

REST API は最も一般的な API 形式で、HTTP メソッドを使ってリソースを操作します。ここでは、実際のユーザー管理 API との連携例をご紹介します。

javascriptconst axios = require('axios');

class UserAPIClient {
  constructor(baseURL, apiKey) {
    this.client = axios.create({
      baseURL,
      timeout: 10000,
      headers: {
        'X-API-Key': apiKey,
        'Content-Type': 'application/json'
      }
    });

    // レスポンス変換の設定
    this.client.interceptors.response.use(
      response => ({
        data: response.data,
        status: response.status,
        headers: response.headers
      }),
      error => this.handleError(error)
    );
  }

  handleError(error) {
    if (error.response) {
      const { status, data } = error.response;
      throw new Error(`API Error ${status}: ${data.message || 'Unknown error'}`);
    }
    throw new Error(`Network Error: ${error.message}`);
  }

  // ユーザー一覧の取得(ページネーション対応)
  async getUsers(page = 1, limit = 10, filters = {}) {
    const params = {
      page,
      limit,
      ...filters
    };

    const response = await this.client.get('/users', { params });
    return {
      users: response.data.users,
      pagination: {
        currentPage: response.data.page,
        totalPages: response.data.total_pages,
        totalUsers: response.data.total_count
      }
    };
  }

  // 特定ユーザーの詳細取得
  async getUserById(userId) {
    if (!userId) {
      throw new Error('User ID is required');
    }

    const response = await this.client.get(`/users/${userId}`);
    return response.data;
  }
}

以下は、より複雑な検索機能を持つ API クライアントの実装例です:

javascript// 高度な検索とフィルタリング機能
class AdvancedUserAPIClient extends UserAPIClient {
  // 複数条件での検索
  async searchUsers(searchCriteria) {
    const {
      name,
      email,
      department,
      role,
      createdAfter,
      createdBefore,
      isActive
    } = searchCriteria;

    const params = {};
    
    // 条件が指定されている場合のみパラメータに追加
    if (name) params.name = name;
    if (email) params.email = email;
    if (department) params.department = department;
    if (role) params.role = role;
    if (createdAfter) params.created_after = createdAfter;
    if (createdBefore) params.created_before = createdBefore;
    if (typeof isActive === 'boolean') params.is_active = isActive;

    const response = await this.client.get('/users/search', { params });
    return response.data;
  }

  // バッチ処理でのユーザー作成
  async createUsersInBatch(users) {
    if (!Array.isArray(users) || users.length === 0) {
      throw new Error('Users array is required and must not be empty');
    }

    // バッチサイズを制限(API の制限に応じて調整)
    const batchSize = 50;
    const results = [];

    for (let i = 0; i < users.length; i += batchSize) {
      const batch = users.slice(i, i + batchSize);
      
      try {
        const response = await this.client.post('/users/batch', {
          users: batch
        });
        
        results.push(...response.data.created_users);
      } catch (error) {
        console.error(`Batch ${Math.floor(i / batchSize) + 1} failed:`, error.message);
        throw error;
      }
    }

    return results;
  }
}

GraphQL API との連携実装

GraphQL は、必要なデータだけを効率的に取得できる柔軟な API 仕様です。REST と比べてオーバーフェッチやアンダーフェッチを避けることができます。

javascriptconst axios = require('axios');

class GraphQLClient {
  constructor(endpoint, options = {}) {
    this.endpoint = endpoint;
    this.defaultHeaders = {
      'Content-Type': 'application/json',
      ...options.headers
    };
  }

  async query(query, variables = {}, options = {}) {
    try {
      const response = await axios.post(this.endpoint, {
        query,
        variables
      }, {
        headers: {
          ...this.defaultHeaders,
          ...options.headers
        },
        timeout: options.timeout || 10000
      });

      // GraphQL エラーの処理
      if (response.data.errors) {
        const errorMessages = response.data.errors.map(err => err.message).join(', ');
        throw new Error(`GraphQL Error: ${errorMessages}`);
      }

      return response.data.data;
    } catch (error) {
      if (error.response) {
        throw new Error(`HTTP ${error.response.status}: ${error.response.statusText}`);
      }
      throw error;
    }
  }

  // ユーザー情報の取得
  async getUser(userId, fields = ['id', 'name', 'email']) {
    const query = `
      query GetUser($userId: ID!) {
        user(id: $userId) {
          ${fields.join('\n          ')}
        }
      }
    `;

    const data = await this.query(query, { userId });
    return data.user;
  }
}

より実践的な GraphQL クライアントの実装例です:

javascriptclass EnhancedGraphQLClient extends GraphQLClient {
  constructor(endpoint, options = {}) {
    super(endpoint, options);
    this.cache = new Map();
    this.cacheTimeout = options.cacheTimeout || 300000; // 5分
  }

  // キャッシュ機能付きクエリ
  async cachedQuery(query, variables = {}, cacheKey = null) {
    const key = cacheKey || this.generateCacheKey(query, variables);
    const cached = this.cache.get(key);

    if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
      return cached.data;
    }

    const data = await this.query(query, variables);
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    });

    return data;
  }

  generateCacheKey(query, variables) {
    return btoa(query + JSON.stringify(variables));
  }

  // 複雑なクエリの実装例
  async getUsersWithPosts(limit = 10, offset = 0) {
    const query = `
      query GetUsersWithPosts($limit: Int!, $offset: Int!) {
        users(limit: $limit, offset: $offset) {
          edges {
            node {
              id
              name
              email
              createdAt
              posts(limit: 5) {
                edges {
                  node {
                    id
                    title
                    excerpt
                    publishedAt
                  }
                }
              }
            }
          }
          pageInfo {
            hasNextPage
            hasPreviousPage
            startCursor
            endCursor
          }
        }
      }
    `;

    return this.cachedQuery(query, { limit, offset });
  }

  // ミューテーションの実装
  async createUser(userData) {
    const mutation = `
      mutation CreateUser($input: CreateUserInput!) {
        createUser(input: $input) {
          user {
            id
            name
            email
            createdAt
          }
          errors {
            field
            message
          }
        }
      }
    `;

    const data = await this.query(mutation, { input: userData });
    
    if (data.createUser.errors.length > 0) {
      const errorMessages = data.createUser.errors.map(err => 
        `${err.field}: ${err.message}`
      ).join(', ');
      throw new Error(`Validation Error: ${errorMessages}`);
    }

    return data.createUser.user;
  }
}

WebSocket API との連携実装

リアルタイムな通信が必要な場合には、WebSocket API を使用します。チャット機能、ライブ更新、ストリーミングデータなどで活用されます。

javascriptconst WebSocket = require('ws');
const EventEmitter = require('events');

class WebSocketClient extends EventEmitter {
  constructor(url, options = {}) {
    super();
    this.url = url;
    this.options = options;
    this.ws = null;
    this.reconnectAttempts = 0;
    this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
    this.reconnectInterval = options.reconnectInterval || 5000;
    this.heartbeatInterval = options.heartbeatInterval || 30000;
    this.heartbeatTimer = null;
  }

  connect() {
    try {
      this.ws = new WebSocket(this.url, this.options);
      
      this.ws.on('open', () => {
        console.log('WebSocket connected');
        this.reconnectAttempts = 0;
        this.startHeartbeat();
        this.emit('connected');
      });

      this.ws.on('message', (data) => {
        try {
          const message = JSON.parse(data.toString());
          this.handleMessage(message);
        } catch (error) {
          console.error('Failed to parse WebSocket message:', error);
          this.emit('error', error);
        }
      });

      this.ws.on('close', (code, reason) => {
        console.log(`WebSocket closed: ${code} ${reason}`);
        this.stopHeartbeat();
        this.emit('disconnected', { code, reason });
        this.attemptReconnect();
      });

      this.ws.on('error', (error) => {
        console.error('WebSocket error:', error);
        this.emit('error', error);
      });

    } catch (error) {
      console.error('Failed to create WebSocket connection:', error);
      this.emit('error', error);
    }
  }

  handleMessage(message) {
    switch (message.type) {
      case 'ping':
        this.send({ type: 'pong' });
        break;
      case 'pong':
        // ハートビート応答を受信
        break;
      default:
        this.emit('message', message);
        break;
    }
  }
}

実用的なチャット機能を想定したより高機能な WebSocket クライアントの実装です:

javascriptclass ChatWebSocketClient extends WebSocketClient {
  constructor(url, authToken, options = {}) {
    super(url, {
      ...options,
      headers: {
        'Authorization': `Bearer ${authToken}`,
        ...options.headers
      }
    });
    
    this.messageQueue = [];
    this.isConnected = false;
    this.subscriptions = new Map();
  }

  connect() {
    super.connect();
    
    this.on('connected', () => {
      this.isConnected = true;
      this.flushMessageQueue();
    });

    this.on('disconnected', () => {
      this.isConnected = false;
    });

    this.on('message', (message) => {
      this.handleChatMessage(message);
    });
  }

  handleChatMessage(message) {
    switch (message.type) {
      case 'chat_message':
        this.emit('chatMessage', message.data);
        break;
      case 'user_joined':
        this.emit('userJoined', message.data);
        break;
      case 'user_left':
        this.emit('userLeft', message.data);
        break;
      case 'subscription_confirmed':
        this.subscriptions.set(message.channel, true);
        break;
      case 'error':
        console.error('Server error:', message.error);
        this.emit('serverError', message.error);
        break;
    }
  }

  // チャンネルの購読
  subscribeToChannel(channelId) {
    const message = {
      type: 'subscribe',
      channel: channelId,
      timestamp: Date.now()
    };

    this.send(message);
  }

  // メッセージ送信(接続状態を考慮)
  sendChatMessage(channelId, content) {
    const message = {
      type: 'chat_message',
      channel: channelId,
      content,
      timestamp: Date.now()
    };

    if (this.isConnected) {
      this.send(message);
    } else {
      this.messageQueue.push(message);
    }
  }

  send(message) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify(message));
    } else {
      console.warn('WebSocket is not open, message queued');
      this.messageQueue.push(message);
    }
  }

  flushMessageQueue() {
    while (this.messageQueue.length > 0) {
      const message = this.messageQueue.shift();
      this.send(message);
    }
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      this.send({ type: 'ping', timestamp: Date.now() });
    }, this.heartbeatInterval);
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  attemptReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++;
      console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
      
      setTimeout(() => {
        this.connect();
      }, this.reconnectInterval);
    } else {
      console.error('Max reconnection attempts reached');
      this.emit('maxReconnectAttemptsReached');
    }
  }

  disconnect() {
    this.stopHeartbeat();
    if (this.ws) {
      this.ws.close();
    }
  }
}

// 使用例
const chatClient = new ChatWebSocketClient(
  'wss://api.example.com/chat',
  process.env.AUTH_TOKEN
);

chatClient.on('connected', () => {
  console.log('Chat client connected');
  chatClient.subscribeToChannel('general');
});

chatClient.on('chatMessage', (message) => {
  console.log(`${message.user}: ${message.content}`);
});

chatClient.on('error', (error) => {
  console.error('Chat client error:', error);
});

chatClient.connect();

まとめ

Node.js でのサードパーティ API 連携は、現代の Web アプリケーション開発において欠かせない技術です。本記事では、実践的な連携パターンと、開発現場でよく直面する課題への対処法をご紹介いたしました。

適切な HTTP クライアントライブラリの選択から始まり、認証方式の実装、エラーハンドリングとリトライ機構の構築まで、堅牢な API 連携を実現するための要素は多岐にわたります。しかし、これらの技術を一つずつ丁寧に実装することで、安定性が高く保守しやすいアプリケーションを構築できます。

特に重要なのは、エラーハンドリングの設計です。外部 API との通信では様々なエラーが発生する可能性があり、それらを適切に分類して処理することで、ユーザーエクスペリエンスの向上とシステムの安定性を両立できます。

また、REST API、GraphQL、WebSocket といった異なる API 形式それぞれに適した実装パターンを理解することで、プロジェクトの要件に最適な選択ができるようになります。

今後も API エコシステムは進化し続けていきますが、本記事で紹介した基本的な考え方と実装パターンは、長期にわたって活用できる知識となるでしょう。継続的な学習と実践を通じて、より良い API 連携の実装を目指していきましょう。

関連リンク