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

APP JavaScript
スポンサーリンク

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

5日目のテーマは
「Datamuse アプリの“中身”を整理して、fetch / async-await / エラーハンドリングを設計として理解する」
ことです。

ここまでであなたは、
単語検索・モード切り替え・サジェスト・履歴・お気に入り・ローカルストレージ
まで作ってきました。

今日はそこから一歩進めて、

  • fetch を共通関数にして「どこからでも同じ型で使える」ようにする
  • Promise / async-await を使って「複数の API を同時に叩く」体験をする
  • エラーハンドリングを「原因別」に整理する
  • ローディング表示を「複数リクエスト」に対応させる

という、まさに中級者らしい内容に入っていきます。

新しい文法はほぼ出てきません。
今までやってきたことを「一段抽象度を上げて」扱う回です。


fetch を「共通関数」にしてしまう発想

なぜ毎回 fetch を直書きしない方がいいのか

今のコードは、
本検索・サジェストなど、いろいろな場所で fetch を書いています。

どこもだいたい同じ形です。

const response = await fetch(url);
if (!response.ok) { … }
const data = await response.json();
JavaScript

これを毎回書くと、

  • エラーハンドリングの書き方がバラバラになる
  • 修正したいときに、全部の fetch を直さないといけない
  • 「fetch のお作法」がコード中に散らばる

という状態になります。

そこで、
「JSON を返す API を叩くための共通関数」
をひとつ作ってしまいます。


共通の requestJson 関数を作る

まずは素直な形で書いてみる

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

    if (!response.ok) {
      throw new Error(`HTTPエラー(${response.status})`);
    }

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

  } catch (error) {
    throw new Error(error.message);
  }
}
JavaScript

この関数は、
「URL とオプションを受け取って、JSON を返す」
という役割だけを持っています。

ポイントは、

  • fetch 自体の失敗(ネットワークエラー)
  • HTTP エラー(404, 500 など)
  • JSON パースエラー

を全部まとめて throw new Error(...) にしていることです。

これにより、
上の階層の関数は「成功したか・失敗したか」だけを扱えばよくなります。


Datamuse 専用の「検索関数」を作る

本検索を requestJson の上に乗せる

今までの本検索は、
fetch を直接書いていました。

これを、
requestJson を使う形に変えます。

async function searchDatamuse(word, mode) {
  const url = `https://api.datamuse.com/words?${mode}=${encodeURIComponent(word)}`;

  const data = await requestJson(url);

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

  if (data.length === 0) {
    throw new Error("該当する単語が見つかりませんでした。");
  }

  const sorted = [...data].sort((a, b) => (b.score || 0) - (a.score || 0));
  return sorted;
}
JavaScript

ここでの深掘りポイントは、

  • requestJson が「通信まわり」を担当
  • searchDatamuse が「Datamuse の仕様チェック」を担当

というふうに、
責務を分けていることです。


UI 側の関数を「ストーリーとして読める」形にする

fetchWords を“ほぼ日本語”にする

共通関数を作ると、
UI 側の関数が一気に読みやすくなります。

async function fetchWords() {
  const word = wordInput.value.trim();
  const mode = modeSelect.value;

  if (!word) {
    statusDiv.textContent = "単語を入力してください。";
    resultDiv.textContent = "";
    return;
  }

  const modeLabel = getModeLabel(mode);

  startLoading(`${modeLabel}を取得中です…`);
  resultDiv.textContent = "";

  try {
    const words = await searchDatamuse(word, mode);

    statusDiv.textContent = `${modeLabel}の取得に成功しました。`;
    renderWords(words, modeLabel);
    addHistory(word, mode);

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

  } finally {
    endLoading();
  }
}
JavaScript

上から読むと、ほぼ物語です。

単語をチェックして
モード名を決めて
ローディングを開始して
Datamuse に検索を投げて
成功したら表示して履歴に残して
失敗したらエラーメッセージを出して
最後にローディングを止める

fetch の細かい話は、
searchDatamuserequestJson に隠れています。


Promise.all で「複数モードを一気に検索」してみる

複数の API を同時に叩くイメージ

ここから少しだけ、
Promise / async-await の“本気”を見てみます。

例えば、
「ある単語について、意味が近い単語と連想語を同時に知りたい」
というケースを考えます。

やりたいことはこうです。

  • ml=wordrel_trg=word を同時に投げる
  • 両方の結果がそろったら画面に表示する
  • どちらかが失敗したら、エラーメッセージを出す

これを実現するのが Promise.all です。


Promise.all を async/await で使う

複数モード検索の関数

async function searchMultipleModes(word) {
  const promises = [
    searchDatamuse(word, "ml"),
    searchDatamuse(word, "rel_trg")
  ];

  const [similar, related] = await Promise.all(promises);

  return { similar, related };
}
JavaScript

ここでのポイントは、

  • searchDatamuse は Promise(非同期処理)を返す
  • Promise.all は「全部終わるまで待つ」
  • 結果は配列で返ってくるので、分割代入で受け取る

という流れです。


複数モード検索を UI に組み込む

ボタンをひとつ増やすイメージ

例えば、
「まとめて検索」ボタンを用意して、
こういう関数をつなぎます。

async function fetchMultiple() {
  const word = wordInput.value.trim();

  if (!word) {
    statusDiv.textContent = "単語を入力してください。";
    resultDiv.textContent = "";
    return;
  }

  startLoading("複数モードで検索中です…");
  resultDiv.textContent = "";

  try {
    const { similar, related } = await searchMultipleModes(word);

    statusDiv.textContent = "複数モードの取得に成功しました。";
    renderMultipleResults(similar, related);
    addHistory(word, "multi");

  } catch (error) {
    statusDiv.textContent = `複数モード検索中にエラーが発生しました:${error.message}`;
    console.error(error);

  } finally {
    endLoading();
  }
}
JavaScript

複数モードの結果を表示する

renderMultipleResults の例

function renderMultipleResults(similar, related) {
  let html = "<h3>複数モード検索結果</h3>";

  html += "<h4>意味が近い単語</h4>";
  similar.forEach((item) => {
    html += `<p>${item.word}(スコア: ${item.score})</p>`;
  });

  html += "<h4>連想される単語</h4>";
  related.forEach((item) => {
    html += `<p>${item.word}(スコア: ${item.score})</p>`;
  });

  resultDiv.innerHTML = html;
}
JavaScript

ここで大事なのは、
「Promise.all を使っても、UI 側の構造は変わらない」
という感覚です。

やっていることは、

  • 非同期処理を待つ
  • 結果を UI に反映する

という、いつものパターンの延長です。


エラーハンドリングを「原因別」に整理する

requestJson を少し賢くする

今の requestJson は、
HTTP ステータスをまとめて HTTPエラー(コード) にしています。

ここを、
少しだけ「原因別」にしてみます。

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

    if (!response.ok) {
      if (response.status === 429) {
        throw new Error("リクエストが多すぎます。少し待ってから再試行してください。");
      }
      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

ここでの深掘りポイントは、

  • 429(Too Many Requests)を特別扱いしている
  • 500 番台を「サーバー側の問題」として扱っている
  • それ以外は汎用的なメッセージにしている

というところです。

ユーザーにとっては、
「自分が悪いのか、サーバーが悪いのか」
がわかるだけで、ストレスがかなり減ります。


ローディング表示を「複数リクエスト」に対応させる

ローディング中かどうかを state で管理する

複数モード検索などをすると、
「今どのリクエストが動いているのか」がわかりにくくなります。

そこで、
ローディング状態を state に持たせておきます。

const state = {
  isLoading: false,
  history: [],
  favorites: []
};
JavaScript

そして、
startLoading / endLoading で更新します。

function startLoading(message) {
  updateState({ isLoading: true });
  statusDiv.textContent = message || "処理中です…";
  searchButton.disabled = true;
  multiButton.disabled = true;
}

function endLoading() {
  updateState({ isLoading: false });
  searchButton.disabled = false;
  multiButton.disabled = false;
}
JavaScript

これで、

  • 単一モード検索
  • 複数モード検索

どちらでも、
「ローディング中はボタンが無効になる」
という一貫した動きになります。


5日目のまとめ

5日目でやったことを、言葉で整理してみます。

fetch を共通関数 requestJson にまとめて、
「通信まわりのエラーハンドリング」を一箇所に集約した。

Datamuse 専用の searchDatamuse を作り、
API の仕様チェック(配列かどうか、件数があるかなど)をそこで行うようにした。

UI 側の fetchWords は、
「入力チェック → ローディング → searchDatamuse → 表示 → 履歴 → ローディング終了」
という“ストーリーとして読める”形になった。

Promise.all を使って、
複数モードを同時に検索する searchMultipleModes を作った。

複数モード検索でも、
結局は「非同期処理を待って UI に反映する」という
いつものパターンの延長であることを確認した。

requestJson の中で、
429 や 500 番台などを「原因別」に扱うことで、
ユーザーにとってわかりやすいエラーメッセージにした。

ローディング状態を state に持たせ、
単一検索・複数検索どちらでも一貫した UI にした。


今日いちばん深く理解してほしいこと

5日目の本質は、

「fetch / async/await / try-catch は、“型”として設計してしまえば怖くない」
ということです。

単一検索
サジェスト
履歴
お気に入り
複数モード検索

これらは全部、
同じ型の上に乗っています。

6日目・7日目では、
この Datamuse アプリを「中級編の完成形」としてまとめながら、
あなたの中に “API アプリの設計パターン” をしっかり定着させていきます。

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