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

APP JavaScript
スポンサーリンク

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

7日目のテーマは
「Datamuse API アプリを“中級編の完成形”としてまとめる」
ことです。

ここまで 6 日間で、あなたはすでに

  • fetch と Promise / async-await の基本と実戦
  • Datamuse API の複数モード(類義語 / 連想語 / 韻 / 前方一致)
  • ローディング表示と多重リクエスト防止
  • エラーハンドリングのパターン化
  • お気に入り・履歴・最近検索・ローカルストレージ保存
  • 状態管理(state)と責務分離(API / UI / イベント)

を一通り経験しています。

7日目は、それらを

  • ひとつの「完成したアプリ」として整理する
  • fetch / async-await / エラーハンドリングの“自分なりの型”を言語化する
  • 「他の API にもそのまま応用できる形」にまで抽象度を上げる

ところまで持っていきます。


まず「完成形の全体像」をイメージする

アプリとしての振る舞い

7日目の Datamuse アプリは、こんな感じの“完成形”をイメージします。

  • 単語を入力
  • モードを選択(類義語 / 連想語 / 韻)
  • 検索ボタンで Datamuse API から取得
  • ローディング中は状態表示+ボタン無効化
  • 結果はスコア順に表示
  • 各単語を「お気に入り」に追加可能
  • 検索履歴(単語+モード)から再検索
  • 最近検索した単語一覧
  • お気に入り・履歴・最近はローカルストレージに保存
  • 通信失敗時は、モード名付きでわかりやすくエラー表示

技術的には、
「全部 fetch / async/await / エラーハンドリングの応用」
です。


fetch / async-await / エラーハンドリングの“自分の型”を固める

Datamuse 用の最終版 request 関数

7日目の時点で、
requestDatamuse はこのくらいの形になっているのが理想です。

async function requestDatamuse(params) {
  const baseUrl = "https://api.datamuse.com/words";
  const url = `${baseUrl}?${params.toString()}`;

  let response;

  try {
    response = await fetch(url);
  } catch (networkError) {
    throw new Error("ネットワークエラーが発生しました。接続を確認してください。");
  }

  if (!response.ok) {
    throw new Error(`HTTPエラー(${response.status})が発生しました。`);
  }

  let data;

  try {
    data = await response.json();
  } catch (parseError) {
    throw new Error("レスポンスの解析に失敗しました。");
  }

  if (!Array.isArray(data)) {
    throw new Error("予期しないレスポンス形式です。");
  }

  return data;
}
JavaScript

ここで押さえておきたい「型」はこうです。

  • fetch 自体の失敗は try-catch で捕まえて「ネットワークエラー」として扱う
  • HTTP ステータスコードは response.ok でチェックして、エラーなら throw
  • JSON パースも try-catch で囲んで「解析エラー」として扱う
  • 期待する形式(ここでは配列)でなければ「形式エラー」として扱う

この 4 段階を通して、
「API 通信のすべての失敗を、メッセージ付きの Error に変換して上に投げる」
というのが、あなたの“API 通信の型”です。


状態管理とローディング表示を「筋の通った形」にする

state をアプリの“現在地”として扱う

状態はひとつのオブジェクトにまとめます。

const state = {
  word: "",
  mode: "ml",
  isLoading: false,
  favorites: [],
  history: [],
  recent: []
};

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

ローディング開始・終了の統一

function setStatus(message, type) {
  statusDiv.textContent = message;
  statusDiv.className = "status " + type;
}

function showLoading(message) {
  setStatus(message || "取得中です…", "loading");
}

function showSuccess(message) {
  setStatus(message, "success");
}

function showError(message) {
  setStatus(message, "error");
}

function startLoading(message) {
  updateState({ isLoading: true });
  showLoading(message || "取得中です…");
  searchButton.disabled = true;
}

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

これで、

  • 「ローディング中かどうか」は state.isLoading
  • 見た目は showLoading / showSuccess / showError
  • ボタンの無効化は startLoading / endLoading

という“お作法”が完全に固まります。


検索処理を「ストーリーとして読める」形にする

URL 組み立てと検索本体の分離

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 getModeShortLabel(mode) {
  if (mode === "ml") return "類義語";
  if (mode === "rel_trg") return "連想語";
  if (mode === "rel_rhy") return "韻を踏む単語";
  return "関連語";
}
JavaScript

検索のメイン関数(完成形)

async function fetchWordsForMode(word, mode) {
  if (state.isLoading) return;

  startLoading("単語を取得中です…");
  resultDiv.textContent = "";

  try {
    const params = buildSearchParams(word, mode);
    const data = await requestDatamuse(params);

    if (data.length === 0) {
      const label = getModeShortLabel(mode);
      showError(`${label}が見つかりませんでした。`);
      resultDiv.textContent = "";
      return;
    }

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

    const label = getModeShortLabel(mode);
    showSuccess(`${label}の取得に成功しました。`);

    renderWords(sorted, mode);
    addHistory(word, mode);
    addRecent(word);

  } catch (error) {
    const label = getModeShortLabel(mode);
    showError(`${label}の取得中にエラーが発生しました:${error.message}`);
    console.error(error);

  } finally {
    endLoading();
  }
}
JavaScript

この関数を上から読むと、ちゃんと“物語”になっています。

  • すでにローディング中なら何もしない
  • ローディング状態にして画面をクリア
  • パラメータを作って Datamuse にリクエスト
  • 結果が空なら「見つからなかった」と伝える
  • 結果を並べ替えて表示
  • 履歴と最近を更新
  • どこかでエラーが起きたら、モード名付きでメッセージ表示
  • 最後にローディング状態を解除

ここまで来ると、
「fetch の細かいことを意識しなくても、アプリの流れが読める」
状態になっています。


保存機能(localStorage)を含めた“完成した振る舞い”

お気に入り・履歴・最近の保存と復元

7日目では、保存周りも「当たり前のインフラ」として扱います。

function saveFavorites() {
  localStorage.setItem("dm_favorites", JSON.stringify(state.favorites));
}

function loadFavorites() {
  const saved = localStorage.getItem("dm_favorites");
  if (!saved) {
    state.favorites = [];
    return;
  }

  try {
    const parsed = JSON.parse(saved);
    state.favorites = Array.isArray(parsed) ? parsed : [];
  } catch (e) {
    console.error("お気に入りの読み込みに失敗しました", e);
    state.favorites = [];
  }

  renderFavorites();
}

function saveHistory() {
  localStorage.setItem("dm_history", JSON.stringify(state.history));
}

function loadHistory() {
  const saved = localStorage.getItem("dm_history");
  if (!saved) {
    state.history = [];
    return;
  }

  try {
    const parsed = JSON.parse(saved);
    state.history = Array.isArray(parsed) ? parsed : [];
  } catch (e) {
    console.error("履歴の読み込みに失敗しました", e);
    state.history = [];
  }

  renderHistory();
}

function saveRecent() {
  localStorage.setItem("dm_recent", JSON.stringify(state.recent));
}

function loadRecent() {
  const saved = localStorage.getItem("dm_recent");
  if (!saved) {
    state.recent = [];
    return;
  }

  try {
    const parsed = JSON.parse(saved);
    state.recent = Array.isArray(parsed) ? parsed : [];
  } catch (e) {
    console.error("最近検索の読み込みに失敗しました", e);
    state.recent = [];
  }

  renderRecent();
}
JavaScript

起動時に一度だけ呼びます。

function init() {
  loadFavorites();
  loadHistory();
  loadRecent();
  showSuccess("Datamuse 単語ツールを開始しました。");
}

init();
JavaScript

ここでのポイントは、

  • 保存と読み込みを必ず関数に分けている
  • 読み込み時は try-catch で「壊れたデータ」に備えている
  • 起動時の流れが init() で一目でわかる

という「アプリとしての筋の良さ」です。


通信失敗時の分岐を“自分のルール”として言語化する

あなたの Datamuse アプリのルール

7日目の時点で、あなたのアプリはこんなルールで動いています。

  • ネットワークエラー
    → 「接続を確認してください」とメッセージ
  • HTTP エラー
    → 「HTTPエラー(ステータス)」としてメッセージ
  • JSON パースエラー
    → 「レスポンスの解析に失敗しました」とメッセージ
  • データ形式エラー
    → 「予期しないレスポンス形式です」とメッセージ
  • データは正常だが件数 0
    → 「◯◯が見つかりませんでした」とモード名付きでメッセージ

そしてそれらはすべて、
requestDatamusefetchWordsForMode の流れの中で
「ユーザーにとって意味のある日本語」に変換されている
状態です。

これが、
「エラーハンドリングができている」
ということです。


7日目のミニチャレンジ(自分で考えてみる用)

ここまで来たあなたなら、
次のような拡張も自力で設計できるはずです。

  • モードに「前方一致(sp)」を追加して、
    「この文字で始まる単語を探す」モードを増やす
  • お気に入り単語をクリックすると、その単語で再検索する
  • 検索結果の単語をクリックしたら「その単語を中心に再検索」する
  • 「今日検索した回数」をカウントして表示する

どれも新しい文法は必要ありません。
今あなたが持っている

  • fetch
  • async/await
  • try-catch
  • state
  • localStorage

だけで、全部実現できます。


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

7日目の本質は、

「もう“API を叩ける人”ではなく、“API を前提にアプリを設計できる人”になっている」
ということです。

Datamuse を WeatherAPI に変えても、
NewsAPI に変えても、
やることは同じです。

  • レスポンスの形を理解する
  • fetch / async/await / エラーハンドリングの型に乗せる
  • 状態を整理して UI に反映する
  • 必要なら localStorage で時間をまたぐ

この 7 日間で身につけたのは、
単なる「Datamuse の使い方」ではなく、
「API 通信アプリの設計そのもの」です。

もし「この部分を他の API でやってみたい」みたいなアイデアが浮かんでいたら、
それはもう、次のステージに進む準備ができているサインです。

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