JavaScript | 1 日 120 分 × 7 日アプリ学習:API通信アプリ(ExchangeRate.host)

APP JavaScript
スポンサーリンク

7日目のゴールと今日やること

7日目のテーマは
「ExchangeRate.host 通貨変換アプリを“中級編の完成形”としてまとめる」
ことです。

ここまで 6 日間で、あなたはすでにかなり本格的なアプリを作っています。

金額入力
通貨ペア選択
レート取得と変換
ローディング表示
エラーハンドリング
逆変換ボタン
プリセット通貨ペア
お気に入り通貨ペア(state + localStorage)
複数通貨一括変換(Promise.all)
fetch の共通化(requestJson)
ExchangeRate.host 専用関数(convertViaAPI)

7日目は、これらを「ひとつの完成したアプリ」として整理しながら、
fetch / async‑await / エラーハンドリングの型を、自分の中に定着させる日 です。


アプリ全体の構造をもう一度“言葉で”整理する

4つの層で考える

あなたの通貨変換アプリは、次の 4 層でできています。

API 通信層

ExchangeRate.host と話す、一番下の層です。

async function requestJson(url) {
  try {
    const response = await fetch(url);

    if (!response.ok) {
      if (response.status >= 500) {
        throw new Error("サーバー側でエラーが発生しています。時間をおいて再試行してください。");
      }
      throw new Error(`HTTPエラー(${response.status})`);
    }

    const data = await response.json();
    return data;

  } catch (error) {
    throw new Error(error.message);
  }
}
JavaScript
async function convertViaAPI(from, to, amount) {
  const url =
    `https://api.exchangerate.host/convert` +
    `?from=${encodeURIComponent(from)}` +
    `&to=${encodeURIComponent(to)}` +
    `&amount=${encodeURIComponent(amount)}`;

  const data = await requestJson(url);

  if (!data || data.success === false) {
    throw new Error("レートの取得に失敗しました。");
  }

  if (typeof data.result !== "number") {
    throw new Error("予期しない形式のデータが返されました。");
  }

  return {
    converted: data.result,
    rate: data.info?.rate
  };
}
JavaScript

ここは「ExchangeRate.host の仕様を知っている場所」です。


状態管理層(state)

アプリの“今”を表す情報をまとめて持つ層です。

const state = {
  isLoading: false,
  favorites: [],
  lastResult: null
};

function updateState(updates) {
  Object.assign(state, updates);
}
JavaScript

ここに、

  • ローディング中かどうか
  • お気に入り通貨ペア
  • 最後の変換結果

などが集まります。


UI 更新層

state の内容を画面に反映する層です。

function renderConversion(from, to, amount, converted, rate) {
  const rateText =
    typeof rate === "number"
      ? `1 ${from} = ${rate} ${to}`
      : "";

  const html = `
    <h3>通貨変換結果</h3>
    <p>${amount} ${from} = ${converted} ${to}</p>
    <p>${rateText}</p>
    <button id="favAddButton">★ この通貨ペアをお気に入りに追加</button>
  `;

  resultDiv.innerHTML = html;

  const favButton = document.getElementById("favAddButton");
  favButton.addEventListener("click", () => {
    addFavorite(from, to);
  });
}
JavaScript
function renderFavorites() {
  if (!state.favorites.length) {
    favoritesDiv.textContent = "お気に入り通貨ペアはまだありません。";
    return;
  }

  let html = "<h3>お気に入り通貨ペア</h3>";

  state.favorites.forEach((item, index) => {
    html += `<p data-index="${index}" class="favorite-item">
      ${item.from}${item.to}
    </p>`;
  });

  favoritesDiv.innerHTML = html;

  const items = favoritesDiv.querySelectorAll(".favorite-item");
  items.forEach((el) => {
    el.addEventListener("click", () => {
      const index = Number(el.dataset.index);
      const fav = state.favorites[index];

      fromSelect.value = fav.from;
      toSelect.value = fav.to;

      convertCurrency();
    });
  });
}
JavaScript

イベント処理層

ユーザー操作に応じて、上の層を呼び出す層です。

convertButton.addEventListener("click", convertCurrency);
swapButton.addEventListener("click", swapCurrencies);
presetButtons.forEach((btn) => {
  btn.addEventListener("click", () => {
    const from = btn.dataset.from;
    const to = btn.dataset.to;
    fromSelect.value = from;
    toSelect.value = to;
    statusDiv.textContent = `${from}${to} を選択しました。`;
  });
});
JavaScript

この 4 層構造を意識できている時点で、
あなたはもう「ただ書く人」ではなく「設計できる人」の側にいます。


fetch / async‑await / エラーハンドリングの“型”を完成形として眺める

単一通貨変換の完成形

async function convertCurrency() {
  const rawAmount = amountInput.value.trim();
  const from = fromSelect.value;
  const to = toSelect.value;

  if (from === to) {
    statusDiv.textContent = "異なる通貨を選択してください。";
    resultDiv.textContent = "";
    return;
  }

  const parsed = parseAmount(rawAmount);
  if (!parsed.ok) {
    statusDiv.textContent = parsed.message;
    resultDiv.textContent = "";
    return;
  }

  const amount = parsed.value;

  setLoading(true, `${amount} ${from}${to} に変換中です…`);
  resultDiv.textContent = "";

  try {
    const { converted, rate } = await convertViaAPI(from, to, amount);

    updateState({ lastResult: { from, to, amount, converted, rate } });
    statusDiv.textContent = "通貨変換に成功しました。";
    renderConversion(from, to, amount, converted, rate);

  } catch (error) {
    statusDiv.textContent = `変換中にエラーが発生しました:${error.message}`;
    console.error(error);

  } finally {
    setLoading(false);
  }
}
JavaScript

この関数を、上から日本語で読んでみてください。

金額と通貨ペアを入力欄から取り出す。
同じ通貨ならエラーメッセージを出して終わる。
金額が正しいかチェックする。ダメならメッセージを出して終わる。
ローディングを開始し、結果表示を一旦クリアする。
ExchangeRate.host から変換結果を取得する。
成功したら state に保存し、メッセージを出し、画面に描画する。
失敗したらエラーメッセージを出す。
最後にローディングを終了する。

これが「ストーリーとして読めるコード」です。
fetch の細かい話は、すべて下の層に隠れています。


複数通貨一括変換を“Promise.all の型”として理解する

複数通貨変換の関数

async function convertToMultiple(from, amount, targets) {
  const promises = targets.map((to) =>
    convertViaAPI(from, to, amount)
  );

  const results = await Promise.all(promises);

  return results.map((res, index) => ({
    from,
    to: targets[index],
    amount,
    converted: res.converted,
    rate: res.rate
  }));
}
JavaScript

ここでやっていることは、とてもシンプルです。

通貨コードの配列から、convertViaAPI を並べた Promise の配列を作る。
Promise.all で全部終わるまで待つ。
結果として「通貨ごとの変換結果の配列」を返す。

UI 側から使うとこうなる

async function convertToPopularCurrencies() {
  const rawAmount = amountInput.value.trim();
  const from = fromSelect.value;

  const parsed = parseAmount(rawAmount);
  if (!parsed.ok) {
    statusDiv.textContent = parsed.message;
    return;
  }

  const amount = parsed.value;
  const targets = ["USD", "EUR", "GBP"];

  setLoading(true, `${amount} ${from} を複数通貨に変換中です…`);
  resultDiv.textContent = "";

  try {
    const results = await convertToMultiple(from, amount, targets);

    updateState({ lastResult: results });
    renderMultiple(results);

    statusDiv.textContent = "複数通貨への変換に成功しました。";

  } catch (error) {
    statusDiv.textContent = `変換中にエラーが発生しました:${error.message}`;
    console.error(error);

  } finally {
    setLoading(false);
  }
}
JavaScript

ここでも、上から順に読むとストーリーになっています。

金額をチェックする。
変換したい通貨のリストを決める。
ローディングを開始する。
複数通貨の変換を同時に行う。
成功したら state に保存し、一覧表示を行う。
失敗したらエラーメッセージを出す。
最後にローディングを終了する。

Promise.all の存在を意識しなくても、
「複数通貨の変換をまとめて行う関数」として読めるのが理想です。


ローディング表示と state を“アプリ全体のルール”にする

setLoading の完成形

function setLoading(isLoading, message) {
  updateState({ isLoading });

  if (isLoading) {
    statusDiv.textContent = message || "処理中です…";
  }

  convertButton.disabled = isLoading;
  swapButton.disabled = isLoading;

  const presetButtons = document.querySelectorAll(".preset");
  presetButtons.forEach((btn) => (btn.disabled = isLoading));

  const multiButton = document.getElementById("multiButton");
  if (multiButton) multiButton.disabled = isLoading;
}
JavaScript

ここで大事なのは、

「ローディング中は、ユーザーが混乱しそうな操作を全部止める」
というルールをコードにしていることです。

単一通貨変換でも、複数通貨変換でも、
setLoading(true, …) と setLoading(false) を呼ぶだけで、
同じ体験が提供されます。


エラーハンドリングを“責任の場所”で分けて考える

7日目では、エラーハンドリングをもう一段整理しておきます。

入力エラーは parseAmount の責任です。
ここでは「金額が空か」「数値か」「0 より大きいか」「大きすぎないか」をチェックし、
ダメなら ok: false とメッセージを返します。

HTTP エラーとネットワークエラーは requestJson の責任です。
ここではステータスコードを見て、
500 番台なら「サーバー側の問題」、
それ以外なら一般的な HTTP エラーとして Error を投げます。

API レベルの失敗とデータ形式エラーは convertViaAPI の責任です。
success が false なら「レートの取得に失敗しました」、
result が数値でなければ「予期しない形式」として Error を投げます。

UI 側の関数(convertCurrency や convertToPopularCurrencies)は、
try の中で「うまくいったときのストーリー」を書き、
catch で error.message をユーザー向けに表示します。

この分担ができていると、
「どの種類のエラーがどこで処理されているか」が明確になります。
結果として、コードが直しやすく、拡張しやすくなります。


7日目のまとめと、あなたがもう持っている力

ここまでで、ExchangeRate.host 通貨変換アプリはこういう姿になりました。

単一通貨の変換ができる。
逆変換ボタンで from / to を一瞬で入れ替えられる。
プリセット通貨ペアで、よく使う組み合わせをワンクリックで選べる。
金額入力のチェックがしっかりしている。
ローディング表示が一貫して動く。
入力エラー、HTTP エラー、API エラー、データ形式エラー、ネットワークエラーを、原因別に扱えている。
お気に入り通貨ペアを保存し、localStorage に永続化できる。
お気に入りから再変換できる。
複数通貨への一括変換を Promise.all で同時に行い、一覧表示できる。
state を中心に、fetch、UI、イベントが整理されている。
fetch は requestJson に共通化され、ExchangeRate.host 専用の convertViaAPI によって API 仕様が一箇所に集約されている。

これらはすべて
fetch / async‑await / エラーハンドリング
という 3 つの柱の上に成り立っています。

今日いちばん伝えたいのは、
「ここまで来たあなたは、もう“どんな API でも同じ型で扱える”ところまで来ている」
ということです。

URL が違うだけ。
返ってくる JSON の形が違うだけ。
やっていることの本質は、為替レートでも、祝日でも、天気でも、翻訳でも同じです。

fetch で取りに行く。
async/await で待つ。
try/catch で失敗を受け止める。
state に反映する。
UI に描画する。

この流れを、自分の言葉で説明できるようになっていたら、
もう「API が怖い初心者」ではありません。

ここから先は、
あなたが作りたいアプリに合わせて、
この型の上に機能を積み上げていくだけです。

タイトルとURLをコピーしました