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"; // 念のためのデフォルト
}
JavaScriptURL を「モードに応じて組み立てる」関数を作る
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;
}
JavaScriptfetchRelatedWords をこう書き換えます。
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 "関連語";
}
JavaScriptfetchRelatedWords の中でこう使います。
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;
}
JavaScriptcatch でも同様に。
} 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 アプリに
- 入力補完(前方一致)
- スコア順の並び替え
- 「お気に入り単語」機能
などを足して、
「自分の語彙を増やすためのツール」に育てていきましょう。


