レスポンス全体を1つの観測値として扱う
メタデータのないレートは解釈できない。日中の現物観測、日次参照値、派生クロスのどれかもしれない。rates.EUR だけを抜き出さず、オブジェクト全体を保存する。APIリファレンスでレスポンス項目を確認でき、メソドロジーでsourceと市場状態ラベルの決まり方を確認できる。
json · abbreviated latest responsecopy
{
"base": "USD",
"source": "live",
"market_session": "open",
"timestamp": "2026-08-23T09:42:06Z",
"data_updated_at": "2026-08-23T09:41:00Z",
"rates": { "EUR": 0.86124, "GBP": 0.74281 },
"sources": { "EUR": "live", "GBP": "live" },
"derived_symbols": []
}source は複数通貨レスポンスで最も鮮度の低いデータ区分を要約する。通貨ごとには sources を読み、derived_symbols で他通貨を経由した計算値を確認する。
2つの時刻は意味が違う
timestamp はAPIがレスポンスを組み立てた時刻、data_updated_at は元の観測値が書き込まれた時刻。鮮度表示には後者を使う。
タイマーを書く前に更新方針を決める
万能な更新間隔はない。店舗の概算表示なら数分の共有キャッシュ、運用画面なら表示中の短周期更新、日次レポートなら必要な公表後の1回取得が考えられる。毎回の描画で要求しても同じ観測値が返ることがある。
用途アプリ側の方針表示内容
店舗の概算表示共有キャッシュ、数分ごとに更新通貨、参考値表示、観測時刻
運用画面短いキャッシュ、表示中に更新レート、source、session、時刻
日次レポート必要な公表後に1回取得レートと保存した観測時刻
決済額参考レートを使わない決済・取引事業者の結果
market_session は open、weekend、interbank_closed のいずれか。活発な通貨は取引週に動き、日次参照系列は公表予定に従う。
サーバー側に小さなキャッシュを作る
通貨コードを並べて安定したキーを作り、成功したレスポンスだけを保存する。同時更新は1つにまとめ、認証付き要求とキーはサーバーに置く。
typescript · latest-rates.tscopy
type RateSource = "live" | "ecb_daily" | "fred_daily";
type LatestRates = {
base: string;
source: RateSource;
sources: Record<string, RateSource>;
market_session: "open" | "weekend" | "interbank_closed";
timestamp: string;
data_updated_at: string;
rates: Record<string, number>;
derived_symbols: string[];
};
type CacheEntry = {
value: LatestRates;
fetchedAt: number;
};
const cache = new Map<string, CacheEntry>();
const pending = new Map<string, Promise<LatestRates>>();
function cacheKey(base: string, symbols: string[]): string {
return `${base}:${[...symbols].sort().join(",")}`;
}
async function fetchLatest(base: string, symbols: string[]): Promise<LatestRates> {
const url = new URL(`https://api.exchangerate.dev/v1/latest/${base}`);
url.searchParams.set("symbols", [...symbols].sort().join(","));
const response = await fetch(url, {
headers: process.env.EXCHANGERATE_API_KEY
? { Authorization: `Bearer ${process.env.EXCHANGERATE_API_KEY}` }
: {},
signal: AbortSignal.timeout(8000),
});
if (!response.ok) {
throw new Error(`FX refresh failed: ${response.status}`);
}
return response.json() as Promise<LatestRates>;
}
export async function getLatestRates(
base: string,
symbols: string[],
maxAgeMs = 60_000,
): Promise<LatestRates> {
const key = cacheKey(base, symbols);
const existing = cache.get(key);
if (existing && Date.now() - existing.fetchedAt < maxAgeMs) {
return existing.value;
}
const inFlight = pending.get(key);
if (inFlight) return inFlight;
const refresh = fetchLatest(base, symbols)
.then((value) => {
cache.set(key, { value, fetchedAt: Date.now() });
return value;
})
.finally(() => pending.delete(key));
pending.set(key, refresh);
return refresh;
}複数インスタンスでは共有ストアかフレームワークのキャッシュを使う。値と観測メタデータを一緒に保持し、成功時だけ置換する原則は同じだ。
更新失敗は新しいレートではない
タイムアウト、429、上流エラーは市場観測ではない。最終正常値を許す機能なら、元の data_updated_at とフォールバック状態を示す。正常値がなければエラーにする。429では Retry-After に従い、一時障害にはジッター付き指数バックオフを使う。401は設定修正が必要だ。
typescript · explicit stale fallbackcopy
type ConverterResult =
| { status: "fresh"; data: LatestRates }
| { status: "last_known_good"; data: LatestRates; reason: string };
async function getConverterRates(
base: string,
symbols: string[],
): Promise<ConverterResult> {
const key = cacheKey(base, symbols);
try {
return { status: "fresh", data: await getLatestRates(base, symbols) };
} catch (error) {
const previous = cache.get(key);
if (!previous) throw error;
return {
status: "last_known_good",
data: previous.value,
reason: error instanceof Error ? error.message : "Refresh failed",
};
}
}429レスポンスでは Retry-After があれば従い、全インスタンスが同時に再試行しないようにする。一時障害にはジッター付き指数バックオフを使う。認証エラーは再試行ではなく設定修正が必要だ。
市場休場は正常な状態
market_session: weekend のレスポンスが取引週最後の観測値を持つのは正しい。新しいHTTP応答時刻で観測時刻を上書きしたり、週末レートを作ったりしない。
市場休場中はアプリ側キャッシュを長くしてもよいが、取引週の再開後には更新する。すべての通貨が同じ予定に従うと決めつけず、返された市場状態と製品要件で判断する。
換算後に一度だけ丸める
基準がUSDなら25 USDからEURへの換算は 25 × rates.EUR。レートの精度を保ち、最終金額だけを対象通貨の規則で丸める。
参考値であり約定値ではない
換算レートは表示、概算、分析向け。実際の請求額はスプレッド、手数料、丸め、実行時刻を含む決済・取引事業者の値を使う。
本番前の確認項目
- 画面に必要な通貨だけを要求する。
- 基準通貨と並べたシンボルでキャッシュキーを作る。
- 成功した完全なレスポンスだけを保存する。
- 同時更新を重複させない。
- データ年齢には
data_updated_at を表示する。 sources、market_session、derived_symbols を保持する。- APIキーはサーバー側に置く。
- 最終正常値には元の時刻と明確な状態を付ける。