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

APP JavaScript
スポンサーリンク

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

2日目のテーマは
「1日目で作った“関連語検索アプリ”を、モード切り替え付きの“言葉遊びツール”に育てる」
ことです。

技術的な柱は、1日目と同じです。

  • fetch
  • Promise / async-await
  • エラーハンドリング

ただし今日は、そこにこういう要素を足します。

  • Datamuse API の“別モード”を使い分ける(類義語・連想語・韻を踏む単語など)
  • モード切り替え UI(ラジオボタン or セレクトボックス)
  • ローディング表示を少し丁寧にする
  • エラーメッセージを「モード込み」でわかりやすくする

1日目のコードを「捨てる」のではなく、
“中心の async 関数はそのままに、周りを強化する” という感覚で進めます。


1日目の「型」をもう一度確認する

中心となる async 関数の形

1日目のコアは、だいたいこんな形でした。

async function fetchRelatedWords(word) {
  statusDiv.textContent = "関連語を取得中です…";
  resultDiv.textContent = "";
  searchButton.disabled = true;

  try {
    const url = `https://api.datamuse.com/words?ml=${encodeURIComponent(word)}`;
    const response = await fetch(url);

    if (!response.ok) {
      statusDiv.textContent = `サーバーエラーが発生しました。(${response.status})`;
      return;
    }

    const data = await response.json();

    if (!Array.isArray(data) || data.length === 0) {
      statusDiv.textContent = "関連する単語が見つかりませんでした。";
      resultDiv.textContent = "";
      return;
    }

    statusDiv.textContent = "関連語の取得に成功しました。";
    renderWords(data);

  } catch (error) {
    statusDiv.textContent = "通信に失敗しました。ネットワークを確認してください。";
    console.error(error);

  } finally {
    searchButton.disabled = false;
  }
}
JavaScript

ここで、すでに大事なポイントは押さえられています。

  • fetch を async/await で扱っている
  • try の中で「成功時の処理」、catch で「通信エラー」、finally で「後片付け」
  • HTTP レベルのエラー(response.ok)と「該当なし」を分けている
  • ローディング中はボタンを無効化している

2日目は、この「型」を崩さずに、
URL の作り方と UI を賢くする 方向で進化させます。


Datamuse API の“モード”を知る

代表的なクエリパラメータ

Datamuse には、いろいろな検索モードがあります。
よく使うのはこのあたりです。

類義語(意味が近い単語)

https://api.datamuse.com/words?ml=happy

ml = means like(〜のような意味)

連想語(その単語から連想される単語)

https://api.datamuse.com/words?rel_trg=happy

rel_trg = “triggered by” のイメージ

韻を踏む単語

https://api.datamuse.com/words?rel_rhy=time

rel_rhy = rhyme(韻)

前方一致(〜で始まる単語)

https://api.datamuse.com/words?sp=pro*

sp = spelling(スペルパターン)

今日は、この中から
「類義語」「連想語」「韻を踏む単語」
の 3 モードを切り替えられるようにします。


モード切り替え UI を作る

ラジオボタンでモードを選ぶ

HTML を少し拡張します。

<input id="wordInput" placeholder="英単語を入力 (例: happy)" />

<div id="modeArea">
  <label>
    <input type="radio" name="mode" value="ml" checked />
    類義語(意味が近い単語)
  </label>
  <label>
    <input type="radio" name="mode" value="rel_trg" />
    連想語(その単語から連想される単語)
  </label>
  <label>
    <input type="radio" name="mode" value="rel_rhy" />
    韻を踏む単語
  </label>
</div>

<button id="searchButton">単語を検索</button>

<div id="status"></div>
<div id="result"></div>

JavaScript でモードを取得します。

const wordInput = document.getElementById("wordInput");
const searchButton = document.getElementById("searchButton");
const statusDiv = document.getElementById("status");
const resultDiv = document.getElementById("result");

function getSelectedMode() {
  const radios = document.querySelectorAll('input[name="mode"]');
  for (const r of radios) {
    if (r.checked) return r.value;
  }
  return "ml"; // 念のためのデフォルト
}
JavaScript

URL を「モードに応じて組み立てる」関数を作る

buildUrl を導入する

1日目は fetchRelatedWords の中で URL を直接書いていましたが、
2日目では URL 組み立て専用の関数を作ります。

function buildUrl(word, mode) {
  const baseUrl = "https://api.datamuse.com/words";

  const params = new URLSearchParams();

  if (mode === "ml") {
    params.set("ml", word);
  } else if (mode === "rel_trg") {
    params.set("rel_trg", word);
  } else if (mode === "rel_rhy") {
    params.set("rel_rhy", word);
  } else {
    params.set("ml", word);
  }

  params.set("max", "20");

  return `${baseUrl}?${params.toString()}`;
}
JavaScript

ここでのポイントは、

  • 「どのモードでも baseUrl は同じ」
  • 「変わるのはクエリパラメータだけ」
  • URLSearchParams を使うと文字列連結より安全で読みやすい

というところです。


中心の async 関数を「モード対応」にする

fetchRelatedWords を少しだけ拡張する

async function fetchRelatedWords(word, mode) {
  statusDiv.textContent = "単語を取得中です…";
  resultDiv.textContent = "";
  searchButton.disabled = true;

  try {
    const url = buildUrl(word, mode);
    const response = await fetch(url);

    if (!response.ok) {
      statusDiv.textContent = `サーバーエラーが発生しました。(${response.status})`;
      return;
    }

    const data = await response.json();

    if (!Array.isArray(data) || data.length === 0) {
      statusDiv.textContent = "該当する単語が見つかりませんでした。";
      resultDiv.textContent = "";
      return;
    }

    statusDiv.textContent = "単語の取得に成功しました。";
    renderWords(data, mode);

  } catch (error) {
    statusDiv.textContent = "通信に失敗しました。ネットワークを確認してください。";
    console.error(error);

  } finally {
    searchButton.disabled = false;
  }
}
JavaScript

ボタン側からはこう呼びます。

searchButton.addEventListener("click", () => {
  const word = wordInput.value.trim();
  const mode = getSelectedMode();

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

  fetchRelatedWords(word, mode);
});
JavaScript

ここで重要なのは、
「モードが増えても、fetch・async/await・try-catch の形は変わらない」
ということです。


モードに応じて表示メッセージを変える

見出しをモードごとに変える

renderWords を少し賢くします。

function getModeLabel(mode) {
  if (mode === "ml") return "類義語(意味が近い単語)";
  if (mode === "rel_trg") return "連想語(その単語から連想される単語)";
  if (mode === "rel_rhy") return "韻を踏む単語";
  return "関連する単語";
}

function renderWords(data, mode) {
  const label = getModeLabel(mode);
  let html = `<h3>${label}</h3>`;

  data.forEach((item) => {
    const word = item.word;
    const score = item.score;
    html += `<p>${word}(スコア: ${score})</p>`;
  });

  resultDiv.innerHTML = html;
}
JavaScript

これで、
モードを変えると見出しも変わり、
「今どのモードで検索しているか」がユーザーに伝わります。


ローディング表示を少し丁寧にする

状態をフラグで持つ

簡単でもいいので、
「今ローディング中かどうか」を変数で持っておくと、
多重起動防止や UI 制御がやりやすくなります。

let isLoading = false;

function startLoading() {
  isLoading = true;
  statusDiv.textContent = "単語を取得中です…";
  searchButton.disabled = true;
}

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

fetchRelatedWords をこう書き換えます。

async function fetchRelatedWords(word, mode) {
  if (isLoading) return;

  startLoading();
  resultDiv.textContent = "";

  try {
    const url = buildUrl(word, mode);
    const response = await fetch(url);

    if (!response.ok) {
      statusDiv.textContent = `サーバーエラーが発生しました。(${response.status})`;
      return;
    }

    const data = await response.json();

    if (!Array.isArray(data) || data.length === 0) {
      statusDiv.textContent = "該当する単語が見つかりませんでした。";
      resultDiv.textContent = "";
      return;
    }

    statusDiv.textContent = "単語の取得に成功しました。";
    renderWords(data, mode);

  } catch (error) {
    statusDiv.textContent = "通信に失敗しました。ネットワークを確認してください。";
    console.error(error);

  } finally {
    endLoading();
  }
}
JavaScript

ここでの深掘りポイントは、
「ローディング状態を変数で持つと、後からいくらでも UI を拡張できる」
ということです。

例えば、
ローディング中は入力欄も無効化する、
ローディング中はボタンのラベルを「取得中…」に変える、
などが簡単にできます。


通信失敗時の分岐を“モード込み”で考える

エラーメッセージに「何をしようとしていたか」を含める

例えば、
「韻を踏む単語を探しているときに失敗した」のと、
「類義語を探しているときに失敗した」のでは、
ユーザーの感覚が少し違います。

そこで、モード名をエラーメッセージに含めます。

function getModeShortLabel(mode) {
  if (mode === "ml") return "類義語";
  if (mode === "rel_trg") return "連想語";
  if (mode === "rel_rhy") return "韻を踏む単語";
  return "関連語";
}
JavaScript

fetchRelatedWords の中でこう使います。

const modeLabel = getModeShortLabel(mode);

if (!response.ok) {
  statusDiv.textContent = `${modeLabel}の取得中にサーバーエラーが発生しました。(${response.status})`;
  return;
}

if (!Array.isArray(data) || data.length === 0) {
  statusDiv.textContent = `${modeLabel}が見つかりませんでした。`;
  resultDiv.textContent = "";
  return;
}
JavaScript

catch でも同様に。

} catch (error) {
  const modeLabel = getModeShortLabel(mode);
  statusDiv.textContent = `${modeLabel}の取得中に通信エラーが発生しました。ネットワークを確認してください。`;
  console.error(error);
}
JavaScript

これで、
「何をしようとして失敗したのか」がユーザーに伝わる
エラーメッセージになります。


2日目の完成コード(重要部分をまとめて)

const wordInput = document.getElementById("wordInput");
const searchButton = document.getElementById("searchButton");
const statusDiv = document.getElementById("status");
const resultDiv = document.getElementById("result");

let isLoading = false;

function getSelectedMode() {
  const radios = document.querySelectorAll('input[name="mode"]');
  for (const r of radios) {
    if (r.checked) return r.value;
  }
  return "ml";
}

function getModeLabel(mode) {
  if (mode === "ml") return "類義語(意味が近い単語)";
  if (mode === "rel_trg") return "連想語(その単語から連想される単語)";
  if (mode === "rel_rhy") return "韻を踏む単語";
  return "関連する単語";
}

function getModeShortLabel(mode) {
  if (mode === "ml") return "類義語";
  if (mode === "rel_trg") return "連想語";
  if (mode === "rel_rhy") return "韻を踏む単語";
  return "関連語";
}

function buildUrl(word, mode) {
  const baseUrl = "https://api.datamuse.com/words";
  const params = new URLSearchParams();

  if (mode === "ml") {
    params.set("ml", word);
  } else if (mode === "rel_trg") {
    params.set("rel_trg", word);
  } else if (mode === "rel_rhy") {
    params.set("rel_rhy", word);
  } else {
    params.set("ml", word);
  }

  params.set("max", "20");

  return `${baseUrl}?${params.toString()}`;
}

function startLoading() {
  isLoading = true;
  statusDiv.textContent = "単語を取得中です…";
  searchButton.disabled = true;
}

function endLoading() {
  isLoading = false;
  searchButton.disabled = false;
}

function renderWords(data, mode) {
  const label = getModeLabel(mode);
  let html = `<h3>${label}</h3>`;

  data.forEach((item) => {
    const word = item.word;
    const score = item.score;
    html += `<p>${word}(スコア: ${score})</p>`;
  });

  resultDiv.innerHTML = html;
}

async function fetchRelatedWords(word, mode) {
  if (isLoading) return;

  startLoading();
  resultDiv.textContent = "";

  try {
    const url = buildUrl(word, mode);
    const response = await fetch(url);

    const modeShort = getModeShortLabel(mode);

    if (!response.ok) {
      statusDiv.textContent = `${modeShort}の取得中にサーバーエラーが発生しました。(${response.status})`;
      return;
    }

    const data = await response.json();

    if (!Array.isArray(data) || data.length === 0) {
      statusDiv.textContent = `${modeShort}が見つかりませんでした。`;
      resultDiv.textContent = "";
      return;
    }

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

  } catch (error) {
    const modeShort = getModeShortLabel(mode);
    statusDiv.textContent = `${modeShort}の取得中に通信エラーが発生しました。ネットワークを確認してください。`;
    console.error(error);

  } finally {
    endLoading();
  }
}

searchButton.addEventListener("click", () => {
  const word = wordInput.value.trim();
  const mode = getSelectedMode();

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

  fetchRelatedWords(word, mode);
});
JavaScript

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

2日目で本当に持ち帰ってほしいのは、この感覚です。

  • fetch・async/await・try-catch の「型」はもう変えなくていい
  • 変わるのは「URL の作り方」と「結果の見せ方」だけ
  • モードが増えても、“中心の非同期関数”はそのまま使い回せる
  • エラーメッセージに「何をしようとしていたか(モード)」を含めると、ぐっと親切になる

Datamuse でも、NewsAPI でも、WeatherAPI でも、
やっていることの本質は同じです。

3日目以降は、この Datamuse アプリに

  • 入力補完(前方一致)
  • スコア順の並び替え
  • 「お気に入り単語」機能

などを足して、
「自分の語彙を増やすためのツール」に育てていきましょう。

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