Guide/通貨換算アプリの設計

通貨換算アプリは為替レートをどう更新しているのか?

完全なレート観測値をキャッシュし、決めた周期で更新し、取得失敗や市場休場時にも本来の観測時刻を保持することで、通貨換算アプリは鮮度を正しく伝えられる。

ERexchangerate.dev·Aug 23, 2026·8分で読めます

信頼できる換算アプリは、画面を描くたびに数値だけを取り直さない。レートを sourcemarket_sessiondata_updated_at と一緒に保存し、短時間再利用して、更新に成功した場合だけ置き換える。レスポンス時刻はAPIが応答した時刻、観測時刻はレート自体が変わった時刻である。

Key points
ratessourcesmarket_sessiondata_updated_at を含む観測値全体を保存する。
データ年齢には data_updated_at を使う。timestamp はレスポンス作成時刻である。
活発な通貨は取引週の日中に更新され、日次参照レートは独自の公表周期で更新される。
週末や休場中は、最後の有効な観測値と元の時刻を保持する。
更新失敗時は日時付きの最終正常値を示し、正常値がなければエラーにする。

レスポンス全体を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_sessionopenweekendinterbank_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 を表示する。
  • sourcesmarket_sessionderived_symbols を保持する。
  • APIキーはサーバー側に置く。
  • 最終正常値には元の時刻と明確な状態を付ける。
ER
exchangerate.dev
FXデータを使う開発者向けの設計・統合ガイド。

関連記事

Referencesourceとmarket_sessionの読み方読む TutorialNext.jsとTypeScriptで為替レートを扱う読む Guide参考レートと約定レート読む
他の比較Fixer vs exchangerate.devOpen Exchange Rates vs exchangerate.devCurrencylayer vs exchangerate.dev
学ぶReading source and market_session in your pipelineIndicative vs executable FX rates: what a rates API actually gives youECB参照レートの解説
ライブレートEUR/USDGBP/USDUSD/JPY