メインコンテンツまでスキップ

STEP 3: 実装のベストプラクティス

このステップでは、JWTを使用した実装のベストプラクティスを学習します。トークンのキャッシュ、更新タイミング、エラーハンドリングなどの実装パターンを理解し、本番環境で安全かつ効率的に運用できるようにします。

トークンのキャッシュ

JWTは1時間有効です。同じトークンを繰り返し使用することで、/auth/exchangeエンドポイントへのリクエスト数を削減し、レート制限に達するリスクを最小限に抑えることができます。

なぜキャッシュが重要か

  • レート制限の回避: /auth/exchangeエンドポイントには100リクエスト/分の制限があります
  • パフォーマンスの向上: トークン取得のオーバーヘッドを削減します
  • コスト削減: 不要なネットワークリクエストを削減します

キャッシュの実装パターン

トークンをメモリにキャッシュし、有効期限が切れる前に新しいトークンを取得します。

Node.jsでの実装例

以下は、トークンのキャッシュと自動更新を実装したNode.jsクラスの例です:

const fetch = require('node-fetch');

/**
* マネーフォワード クラウドのAPI クライアント
*
* APIキーを使用してJWTを取得し、自動的にキャッシュと更新を行います。
*/
class MoneyForwardAPIClient {
/**
* クライアントを初期化する
*
* @param {string} apiKey - Money Forward APIキー
*/
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.biz.moneyforward.com';
this._accessToken = null;
this._tokenExpiresAt = null;
}

/**
* キャッシュされたトークンがまだ有効かを確認する
*
* @returns {boolean} トークンが有効な場合true
* @private
*/
_isTokenValid() {
if (!this._accessToken || !this._tokenExpiresAt) {
return false;
}

// 有効期限の5分前にトークンを更新する(安全マージン)
const now = new Date();
const expiryWithMargin = new Date(this._tokenExpiresAt.getTime() - 5 * 60 * 1000);
return now < expiryWithMargin;
}

/**
* APIキーをJWTに交換し、キャッシュする
*
* @throws {Error} APIキーが無効な場合またはその他のエラーが発生した場合
* @private
*/
async _exchangeApiKey() {
try {
const response = await fetch(`${this.baseUrl}/auth/exchange`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`
},
timeout: 10000
});

if (response.status === 200) {
const data = await response.json();
this._accessToken = data.access_token;
const expiresIn = data.expires_in;

// 有効期限を計算してキャッシュ
this._tokenExpiresAt = new Date(Date.now() + expiresIn * 1000);

console.log(`New JWT token obtained, expires at ${this._tokenExpiresAt}`);

} else if (response.status === 401) {
throw new Error("Invalid API key");
} else if (response.status === 429) {
throw new Error("Rate limit exceeded. Please try again later.");
} else {
const text = await response.text();
throw new Error(`Failed to exchange API key: ${response.status} - ${text}`);
}

} catch (error) {
if (error.message.includes('fetch') || error.type === 'request-timeout') {
throw new Error(`Network error during token exchange: ${error.message}`);
}
throw error;
}
}

/**
* 有効なアクセストークンを取得する
*
* キャッシュされたトークンが有効な場合はそれを返し、
* 無効な場合は新しいトークンを取得します。
*
* @returns {Promise<string>} 有効なJWT
* @throws {Error} APIキーが無効な場合またはトークンの取得に失敗した場合
*/
async getAccessToken() {
if (!this._isTokenValid()) {
await this._exchangeApiKey();
}

return this._accessToken;
}

/**
* マネーフォワード クラウドのAPIを呼び出す
*
* @param {string} method - HTTPメソッド('GET', 'POST', など)
* @param {string} path - APIのパス(例: '/{service}/api/v1/...')
* @param {Object} options - fetchに渡す追加のオプション
* @returns {Promise<Response>} APIレスポンス
* @throws {Error} APIキーが無効な場合またはAPI呼び出しに失敗した場合
*/
async callApi(method, path, options = {}) {
// 有効なトークンを取得
const accessToken = await this.getAccessToken();

// Authorizationヘッダーを設定
const headers = options.headers || {};
headers['Authorization'] = `Bearer ${accessToken}`;

// APIを呼び出す
const url = `${this.baseUrl}${path}`;

try {
const response = await fetch(url, {
method,
...options,
headers
});
return response;

} catch (error) {
throw new Error(`API request failed: ${error.message}`);
}
}
}

// 使用例
async function main() {
// 環境変数からAPIキーを取得
const apiKey = process.env.MF_API_KEY;
if (!apiKey) {
throw new Error("MF_API_KEY environment variable is not set");
}

// クライアントを初期化
const client = new MoneyForwardAPIClient(apiKey);

try {
// APIを呼び出す(例: サービスとエンドポイントは実際のものに置き換えてください)
// const response = await client.callApi('GET', '/{service}/api/v1/...');
//
// if (response.status === 200) {
// const data = await response.json();
// console.log("API call successful:", data);
// } else {
// const text = await response.text();
// console.log(`API call failed: ${response.status} - ${text}`);
// }

// 複数回呼び出しても、トークンは自動的にキャッシュされます
// const response2 = await client.callApi('GET', '/{service}/api/v1/...');

} catch (error) {
console.error(`Error: ${error.message}`);
}
}

// 実行例(実際のAPI呼び出しはコメントアウト)
// main();

実装のポイント

1. トークンの有効期限管理

トークンが期限切れになる前に新しいトークンを取得することが重要です:

// 有効期限の5分前にトークンを更新する(安全マージン)
const now = new Date();
const expiryWithMargin = new Date(this._tokenExpiresAt.getTime() - 5 * 60 * 1000);
return now < expiryWithMargin;

なぜ安全マージンが必要か:

  • ネットワーク遅延を考慮
  • 時計のずれを考慮
  • API呼び出し中にトークンが期限切れになるのを防ぐ

2. 並行リクエスト対応

複数の非同期リクエストが同時に発生する場合は、トークン取得を同期します:

class ConcurrentSafeAPIClient extends MoneyForwardAPIClient {
constructor(apiKey) {
super(apiKey);
this._tokenPromise = null;
}

async getAccessToken() {
// 既にトークン取得中の場合は、その Promise を再利用
if (this._tokenPromise) {
return this._tokenPromise;
}

if (!this._isTokenValid()) {
this._tokenPromise = this._exchangeApiKey()
.then(() => {
this._tokenPromise = null;
return this._accessToken;
})
.catch(error => {
this._tokenPromise = null;
throw error;
});
return this._tokenPromise;
}

return this._accessToken;
}
}

3. リトライロジック

一時的なネットワークエラーに対応するため、リトライロジックを実装します:

const fetch = require('node-fetch');

/**
* リトライロジック付きでAPIキーをJWTに交換する
*
* @param {string} apiKey - Money Forward APIキー
* @param {number} maxRetries - 最大リトライ回数
* @param {number} backoffFactor - バックオフ係数
* @returns {Promise<Object>} トークン情報
* @throws {Error} リトライ回数を超えた場合
*/
async function exchangeWithRetry(
apiKey,
maxRetries = 3,
backoffFactor = 2.0
) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('https://api.biz.moneyforward.com/auth/exchange', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`
},
timeout: 10000
});

if (response.status === 200) {
return await response.json();
} else if (response.status === 401) {
// APIキーが無効な場合はリトライしない
throw new Error("Invalid API key");
} else if (response.status === 429) {
// レート制限の場合は長めに待つ
const waitTime = 60;
console.log(`Rate limit exceeded. Waiting ${waitTime} seconds...`);
await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
} else if (response.status >= 500) {
// サーバーエラーの場合はリトライ
const waitTime = Math.pow(backoffFactor, attempt);
console.log(`Server error. Retrying in ${waitTime} seconds...`);
await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
} else {
throw new Error(`Unexpected error: ${response.status}`);
}

} catch (error) {
if (error.message === "Invalid API key") {
throw error;
}

if (attempt < maxRetries - 1) {
const waitTime = Math.pow(backoffFactor, attempt);
console.log(`Network error. Retrying in ${waitTime} seconds...`);
await new Promise(resolve => setTimeout(resolve, waitTime * 1000));
} else {
throw new Error(`Failed after ${maxRetries} attempts: ${error.message}`);
}
}
}

throw new Error(`Failed to exchange API key after ${maxRetries} attempts`);
}

エラーハンドリングのベストプラクティス

1. エラーの分類

エラーを適切に分類し、それぞれに対して適切な対処を行います:

/**
* APIエラーレスポンスを処理する
*
* @param {Response} response - APIレスポンス
* @throws {Error} クライアント側またはサーバー側のエラー
*/
async function handleApiError(response) {
const text = await response.text();

if (response.status >= 500) {
// サーバーエラー: リトライ可能
throw new Error(`Server error: ${response.status} - ${text}`);

} else if (response.status === 429) {
// レート制限: リトライ可能(待機時間あり)
throw new Error("Rate limit exceeded. Please try again later.");

} else if (response.status === 401) {
// 認証エラー: リトライ不可
throw new Error("Authentication failed. Check your API key or token.");

} else if (response.status >= 400) {
// その他のクライアントエラー: リトライ不可
throw new Error(`Client error: ${response.status} - ${text}`);
}
}

2. ログの記録

本番環境では、適切なログを記録して問題の診断を容易にします:

// ロガーの設定(例: Winston, Bunyan, または console を使用)
const logger = console; // 実際の環境では適切なロガーライブラリを使用

class LoggingAPIClient extends MoneyForwardAPIClient {
async _exchangeApiKey() {
logger.info("Exchanging API key for JWT token");

try {
await super._exchangeApiKey();
logger.info("JWT token obtained successfully");

} catch (error) {
if (error.message.includes("Invalid API key")) {
logger.error(`Authentication failed: ${error.message}`);
} else {
logger.error(`Token exchange failed: ${error.message}`);
}
throw error;
}
}

async callApi(method, path, options) {
logger.info(`Calling API: ${method} ${path}`);

try {
const response = await super.callApi(method, path, options);
logger.info(`API call completed: ${response.status}`);
return response;

} catch (error) {
logger.error(`API call failed: ${error.message}`);
throw error;
}
}
}
ログへの機密情報の記録に注意

ログにAPIキーやJWTを記録しないでください。これらは機密情報です。

セキュリティのベストプラクティス

1. APIキーの管理

// 良い例: 環境変数から取得
const apiKey = process.env.MF_API_KEY;

// 悪い例: コードに直接記述
// const apiKey = "mf_api_prd_a1b2c3d4..."; // これはしないでください!

2. トークンの安全な保管

/**
* セキュリティを強化したAPIクライアント
*/
class SecureAPIClient extends MoneyForwardAPIClient {
constructor(apiKey) {
super(apiKey);
// APIキーをメモリから削除(必要に応じて)
this.apiKey = null; // 最初のトークン取得後は不要
}

async getAccessToken() {
// トークンが無効でAPIキーが削除されている場合はエラー
if (!this._isTokenValid() && !this.apiKey) {
throw new Error("API key has been cleared and token has expired");
}

return await super.getAccessToken();
}
}

3. HTTPS通信の確認

const fetch = require('node-fetch');
const https = require('https');

// SSL証明書の検証を必ず有効にする(Node.jsではデフォルトで有効)
const response = await fetch('https://api.biz.moneyforward.com/auth/exchange', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`
},
agent: new https.Agent({
rejectUnauthorized: true // デフォルトでtrueですが、明示的に指定
})
});

まとめ

このステップでは、以下の実装のベストプラクティスを学習しました:

  • トークンのキャッシュ: JWTを1時間キャッシュしてレート制限を回避
  • 有効期限管理: 安全マージンを設けて期限切れを防ぐ
  • エラーハンドリング: エラーを適切に分類し、リトライロジックを実装
  • セキュリティ: APIキーとトークンを安全に管理
  • ログの記録: 問題の診断を容易にするためのログ

次のステップ

これで、APIキーを使用したマネーフォワード クラウドのAPIへのアクセス方法を学習しました。

さらに学ぶ

実装を始める

学習した内容を活かして、実際のアプリケーションで実装を始めましょう。

サポートが必要な場合

実装中に問題が発生した場合は、アプリポータルサポートまでお問い合わせください。