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へのアクセス方法を学習しました。
さらに学ぶ
- APIリファレンス - 利用可能なAPIエンドポイントを確認
- APIキーによる認証の概要 - APIキーの概念を再確認
- /auth/exchangeエンドポイント仕様 - 技術仕様の詳細
実装を始める
学習した内容を活かして、実際のアプリケーションで実装を始めましょう。
サポートが必要な場合
実装中に問題が発生した場合は、アプリポータルサポートまでお問い合わせください。