3日目のゴールと今日やること
3日目のテーマは
「fetch・async/await・エラーハンドリングの“型”はそのままに、Datamuse アプリを“使っていて気持ちいいツール”に近づける」
ことです。
ここまでであなたはすでに、
- 単語を入力して Datamuse API から結果を取得する
- モード(類義語 / 連想語 / 韻)を切り替えて検索する
- ローディング中の状態管理(isLoading)
- 通信失敗時のメッセージ分岐
といった「API 通信アプリの基本フォーム」を手に入れました。
3日目はここに、
- 入力補完(前方一致)っぽい動き
- スコア順の並び替え
- お気に入り単語の簡易実装
- fetch の「再利用しやすい形」への整理
を足して、「語彙を増やすためのミニツール」に育てていきます。
今日の完成イメージを先に描く
どんなアプリにしたいか
3日目の Datamuse アプリは、こんな動きを目指します。
- 単語を入力して検索(類義語 / 連想語 / 韻)
- 結果はスコア順に並ぶ
- 結果の単語をクリックすると「お気に入り」に追加
- 別枠にお気に入り単語一覧が表示される
- 入力欄に文字を打つと、「その文字で始まる候補」を下に表示(簡易サジェスト)
- ローディング中は状態表示と多重起動防止
技術的には、
「やることは全部 fetch・async/await・エラーハンドリングの応用」
です。
まず「共通の fetch 関数」を作る
なぜ共通化するのか
2日目までは、fetchRelatedWords の中で直接 fetch(url) を呼んでいました。
3日目では、
- 通常検索(ml / rel_trg / rel_rhy)
- 入力補完用の前方一致検索(sp)
と、複数の用途で Datamuse を叩きたいので、
「Datamuse にリクエストする関数」を一箇所にまとめます。
Datamuse 用の共通関数
async function requestDatamuse(params) {
const baseUrl = "https://api.datamuse.com/words";
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTPエラー(${response.status})`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error("予期しないレスポンス形式です。");
}
return data;
}
JavaScriptここでのポイントは、
- URL の組み立ては
URLSearchParamsに任せる - HTTP エラーは
throwで上に投げる - 「配列じゃなかったらおかしい」と判断してエラーにする
という「API 通信の型」を一段階抽象化していることです。
通常検索を「共通関数を使う形」に書き換える
検索用のパラメータを組み立てる
function buildSearchParams(word, mode) {
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 params;
}
JavaScript検索関数をシンプルにする
let isLoading = false;
function startLoading(message) {
isLoading = true;
statusDiv.textContent = message || "取得中です…";
searchButton.disabled = true;
}
function endLoading() {
isLoading = false;
searchButton.disabled = false;
}
async function fetchWordsForMode(word, mode) {
if (isLoading) return;
startLoading("単語を取得中です…");
resultDiv.textContent = "";
try {
const params = buildSearchParams(word, mode);
const data = await requestDatamuse(params);
if (data.length === 0) {
const label = getModeShortLabel(mode);
statusDiv.textContent = `${label}が見つかりませんでした。`;
resultDiv.textContent = "";
return;
}
const sorted = [...data].sort((a, b) => (b.score || 0) - (a.score || 0));
const label = getModeShortLabel(mode);
statusDiv.textContent = `${label}の取得に成功しました。`;
renderWords(sorted, mode);
} catch (error) {
const label = getModeShortLabel(mode);
statusDiv.textContent = `${label}の取得中にエラーが発生しました:${error.message}`;
console.error(error);
} finally {
endLoading();
}
}
JavaScriptここでの深掘りポイントは、
- fetch 自体は
requestDatamuseに隠している - 検索関数は「パラメータを作る」「結果を並べる」「表示する」に集中している
- エラーは「どのモードで失敗したか」を含めて表示している
という「役割分担」ができていることです。
結果の単語を「お気に入り」に追加する
お気に入り用の状態を持つ
const favorites = [];
const favoritesDiv = document.getElementById("favorites");
JavaScriptお気に入りに追加する関数
function addFavorite(word) {
if (favorites.includes(word)) {
statusDiv.textContent = "すでにお気に入りに追加されています。";
return;
}
favorites.push(word);
statusDiv.textContent = `「${word}」をお気に入りに追加しました。`;
renderFavorites();
}
JavaScriptお気に入り一覧を表示する
function renderFavorites() {
if (favorites.length === 0) {
favoritesDiv.textContent = "お気に入り単語はまだありません。";
return;
}
let html = "<h3>お気に入り単語</h3>";
favorites.forEach((word) => {
html += `<p>★ ${word}</p>`;
});
favoritesDiv.innerHTML = html;
}
JavaScript結果表示に「お気に入りボタン」を付ける
renderWords を拡張します。
function renderWords(data, mode) {
const label = getModeLabel(mode);
let html = `<h3>${label}</h3>`;
data.forEach((item, index) => {
const word = item.word;
const score = item.score;
html += `
<div class="word-item" data-index="${index}">
<span>${word}(スコア: ${score})</span>
<button class="fav-button" data-word="${word}">★ お気に入り</button>
</div>
`;
});
resultDiv.innerHTML = html;
const buttons = resultDiv.querySelectorAll(".fav-button");
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
const word = btn.dataset.word;
addFavorite(word);
});
});
}
JavaScriptここでのポイントは、
- API のレスポンス(
data)からwordを取り出して、そのままお気に入りに使っている - お気に入りは「単語の配列」というシンプルな形で持っている
という「状態の分離」です。
API の結果とアプリの状態を混ぜないのが大事です。
入力補完(前方一致)っぽい動きを作る
Datamuse の sp パラメータを使う
前方一致は sp を使います。
https://api.datamuse.com/words?sp=pro*&max=5
sp=pro* は「pro で始まる単語」を意味します。
サジェスト用の関数を作る
const suggestDiv = document.getElementById("suggest");
let suggestTimeoutId = null;
async function fetchSuggestions(prefix) {
if (!prefix) {
suggestDiv.textContent = "";
return;
}
const params = new URLSearchParams();
params.set("sp", prefix + "*");
params.set("max", "5");
try {
const data = await requestDatamuse(params);
if (data.length === 0) {
suggestDiv.textContent = "";
return;
}
renderSuggestions(data);
} catch (error) {
console.error("サジェスト取得中にエラー", error);
suggestDiv.textContent = "";
}
}
JavaScriptサジェストの表示
function renderSuggestions(data) {
let html = "<h4>候補:</h4>";
data.forEach((item) => {
const word = item.word;
html += `<button class="suggest-item" data-word="${word}">${word}</button>`;
});
suggestDiv.innerHTML = html;
const buttons = suggestDiv.querySelectorAll(".suggest-item");
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
const word = btn.dataset.word;
wordInput.value = word;
suggestDiv.textContent = "";
});
});
}
JavaScript入力イベントにサジェストを紐づける
wordInput.addEventListener("input", () => {
const value = wordInput.value.trim();
if (suggestTimeoutId) {
clearTimeout(suggestTimeoutId);
}
suggestTimeoutId = setTimeout(() => {
fetchSuggestions(value);
}, 300);
});
JavaScriptここでの深掘りポイントは、
- 入力のたびに即 fetch せず、300ms 待ってから呼ぶ(簡易デバウンス)
- サジェストは「ローディング状態」とは別枠で扱う(メイン検索とは独立)
- それでも中身は結局
requestDatamuseを使っている
という「同じ fetch の型を、用途を変えて何度も使っている」感覚です。
通信失敗時の分岐を「用途ごと」に考える
メイン検索とサジェストでメッセージを変える
メイン検索が失敗したときは、
ユーザーにちゃんと伝える必要があります。
} catch (error) {
const label = getModeShortLabel(mode);
statusDiv.textContent = `${label}の取得中にエラーが発生しました:${error.message}`;
}
JavaScript一方、サジェストは「おまけ機能」に近いので、
失敗しても画面を赤くする必要はありません。
} catch (error) {
console.error("サジェスト取得中にエラー", error);
suggestDiv.textContent = "";
}
JavaScriptここでのポイントは、
- どのエラーも「同じ重さ」で扱わなくていい
- ユーザー体験にとって重要な処理ほど、丁寧にメッセージを出す
という「エラーハンドリングの優先度付け」です。
3日目の全体像(重要部分のコードまとめ)
const wordInput = document.getElementById("wordInput");
const searchButton = document.getElementById("searchButton");
const statusDiv = document.getElementById("status");
const resultDiv = document.getElementById("result");
const favoritesDiv = document.getElementById("favorites");
const suggestDiv = document.getElementById("suggest");
let isLoading = false;
const favorites = [];
let suggestTimeoutId = null;
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 "関連語";
}
async function requestDatamuse(params) {
const baseUrl = "https://api.datamuse.com/words";
const url = `${baseUrl}?${params.toString()}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTPエラー(${response.status})`);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error("予期しないレスポンス形式です。");
}
return data;
}
function buildSearchParams(word, mode) {
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 params;
}
function startLoading(message) {
isLoading = true;
statusDiv.textContent = message || "取得中です…";
searchButton.disabled = true;
}
function endLoading() {
isLoading = false;
searchButton.disabled = false;
}
function addFavorite(word) {
if (favorites.includes(word)) {
statusDiv.textContent = "すでにお気に入りに追加されています。";
return;
}
favorites.push(word);
statusDiv.textContent = `「${word}」をお気に入りに追加しました。`;
renderFavorites();
}
function renderFavorites() {
if (favorites.length === 0) {
favoritesDiv.textContent = "お気に入り単語はまだありません。";
return;
}
let html = "<h3>お気に入り単語</h3>";
favorites.forEach((word) => {
html += `<p>★ ${word}</p>`;
});
favoritesDiv.innerHTML = html;
}
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 += `
<div class="word-item">
<span>${word}(スコア: ${score})</span>
<button class="fav-button" data-word="${word}">★ お気に入り</button>
</div>
`;
});
resultDiv.innerHTML = html;
const buttons = resultDiv.querySelectorAll(".fav-button");
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
const word = btn.dataset.word;
addFavorite(word);
});
});
}
async function fetchWordsForMode(word, mode) {
if (isLoading) return;
startLoading("単語を取得中です…");
resultDiv.textContent = "";
try {
const params = buildSearchParams(word, mode);
const data = await requestDatamuse(params);
if (data.length === 0) {
const label = getModeShortLabel(mode);
statusDiv.textContent = `${label}が見つかりませんでした。`;
resultDiv.textContent = "";
return;
}
const sorted = [...data].sort((a, b) => (b.score || 0) - (a.score || 0));
const label = getModeShortLabel(mode);
statusDiv.textContent = `${label}の取得に成功しました。`;
renderWords(sorted, mode);
} catch (error) {
const label = getModeShortLabel(mode);
statusDiv.textContent = `${label}の取得中にエラーが発生しました:${error.message}`;
console.error(error);
} finally {
endLoading();
}
}
function renderSuggestions(data) {
let html = "<h4>候補:</h4>";
data.forEach((item) => {
const word = item.word;
html += `<button class="suggest-item" data-word="${word}">${word}</button>`;
});
suggestDiv.innerHTML = html;
const buttons = suggestDiv.querySelectorAll(".suggest-item");
buttons.forEach((btn) => {
btn.addEventListener("click", () => {
const word = btn.dataset.word;
wordInput.value = word;
suggestDiv.textContent = "";
});
});
}
async function fetchSuggestions(prefix) {
if (!prefix) {
suggestDiv.textContent = "";
return;
}
const params = new URLSearchParams();
params.set("sp", prefix + "*");
params.set("max", "5");
try {
const data = await requestDatamuse(params);
if (data.length === 0) {
suggestDiv.textContent = "";
return;
}
renderSuggestions(data);
} catch (error) {
console.error("サジェスト取得中にエラー", error);
suggestDiv.textContent = "";
}
}
searchButton.addEventListener("click", () => {
const word = wordInput.value.trim();
const mode = getSelectedMode();
if (!word) {
statusDiv.textContent = "単語を入力してください。";
resultDiv.textContent = "";
return;
}
fetchWordsForMode(word, mode);
});
wordInput.addEventListener("input", () => {
const value = wordInput.value.trim();
if (suggestTimeoutId) {
clearTimeout(suggestTimeoutId);
}
suggestTimeoutId = setTimeout(() => {
fetchSuggestions(value);
}, 300);
});
JavaScript今日いちばん深く理解してほしいこと
3日目の本質は、
- fetch・async/await・エラーハンドリングの「型」はもう変えない
- Datamuse へのリクエストを
requestDatamuseにまとめることで、用途を増やしても迷子にならない - 「お気に入り」「サジェスト」などの機能は、全部この型の上に乗っているだけ
という感覚です。
あなたはもう、
「API を叩ける人」ではなく、「API を前提に機能を設計できる人」になりつつあります。


