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

APP JavaScript
スポンサーリンク

7日目のゴールと全体像

7日目のテーマは
「WeatherAPI を使った API 通信アプリを“中級レベルの完成形”にまとめる」
ことです。

ここまでであなたは、
fetch・Promise・async/await・エラーハンドリング・ローディング表示・履歴・お気に入り・現在地・複数都市比較など、
かなり多くの要素を扱ってきました。

7日目では、それらをバラバラな“機能の寄せ集め”ではなく、
一つのアプリとして筋の通った構造に整理することに集中します。

具体的には、

API 通信部分を共通化する
UI 更新ロジックを整理する
エラーハンドリングを一貫したルールにする
「状態」を中心にコードを読む癖をつける

という視点で、
fetch・async/await・エラーハンドリングをもう一段深く理解していきます。


fetch・async/await・エラーハンドリングの「最終フォーム」を作る

API 通信の共通関数を完成させる

まずは、どの画面からでも使える
「天気データ取得専用の関数」を完成させます。

const API_KEY = "YOUR_API_KEY";
const baseUrl = "https://api.weatherapi.com/v1";

function buildUrl(path, paramsObj) {
  const params = new URLSearchParams({
    key: API_KEY,
    lang: "ja",
    ...paramsObj
  });
  return `${baseUrl}/${path}?${params.toString()}`;
}

async function getWeatherData({ q, days = 1, type = "current" }) {
  const path = type === "forecast" ? "forecast.json" : "current.json";
  const url = buildUrl(path, { q, days });

  const response = await fetch(url);

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

  const data = await response.json();

  if (data.error) {
    throw new Error(data.error.message);
  }

  return data;
}
JavaScript

ここで重要なのは、
「この関数は UI を一切知らない」ということです。

q(都市名や緯度経度)
days(何日分か)
type(current か forecast か)

だけを引数として受け取り、
成功したら data を返し、
失敗したら Error を投げる。

この“純粋な API 関数”を持てると、
アプリ全体の見通しが一気によくなります。


UI 側の非同期処理を「役割ごとに分けて書く」

単一都市・予報表示の最終形

単一都市の 3 日間予報を表示する関数は、
こういう形に落ち着きます。

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

let lastData = null;
let unit = "c";

function validateCity(city) {
  if (!city.trim()) return "都市名を入力してください。";
  if (city.length > 50) return "都市名が長すぎます。50文字以内で入力してください。";
  return null;
}

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

function showLoading() {
  setStatus("天気情報を取得中です…", "loading");
}

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

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

function startLoading() {
  searchButton.disabled = true;
}

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

async function fetchForecastByCity(city) {
  const error = validateCity(city);
  if (error) {
    showError(error);
    resultDiv.textContent = "";
    return;
  }

  showLoading();
  startLoading();

  try {
    const data = await getWeatherData({ q: city, days: 3, type: "forecast" });
    lastData = data;
    showStatus("3日分の天気を取得しました。");
    renderForecast(data);

  } catch (err) {
    showError(`取得に失敗しました:${err.message}`);

  } finally {
    endLoading();
  }
}
JavaScript

ここでのポイントは、

API 通信は getWeatherData に任せる
UI 側は「状態の更新」と「表示」に集中する
try の中は「成功時の処理だけ」にする

という役割分担です。


表示ロジックを「状態 × 表示」に整理する

予報表示を単位切り替え対応でまとめる

function renderForecast(data) {
  const name = data.location.name;
  const country = data.location.country;
  const days = data.forecast.forecastday;

  let html = "";
  html += `<p>${name} (${country}) の 3 日間の天気</p>`;

  days.forEach((day) => {
    const date = day.date;
    const max = unit === "c" ? day.day.maxtemp_c : day.day.maxtemp_f;
    const min = unit === "c" ? day.day.mintemp_c : day.day.mintemp_f;
    const text = day.day.condition.text;
    const iconUrl = "https:" + day.day.condition.icon;
    const unitLabel = unit === "c" ? "℃" : "℉";

    html += `
      <div class="day">
        <p>日付:${date}</p>
        <img src="${iconUrl}" alt="${text}" />
        <p>最高:${max} ${unitLabel} / 最低:${min} ${unitLabel}</p>
        <p>天気:${text}</p>
      </div>
    `;
  });

  resultDiv.innerHTML = html;
}

function rerenderIfPossible() {
  if (lastData) {
    renderForecast(lastData);
  }
}
JavaScript

unit と lastData という「状態」を持ち、
それをもとに renderForecast が UI を作る。

この構造は、
タイマーの state や
お気に入りの favoriteCity と
まったく同じパターンです。


現在地・履歴・お気に入りを「同じ型」で扱う

現在地の天気取得を共通関数で書き直す

function getCurrentLocationWeather() {
  showLoading();
  startLoading();

  navigator.geolocation.getCurrentPosition(
    async (pos) => {
      const lat = pos.coords.latitude;
      const lon = pos.coords.longitude;

      try {
        const data = await getWeatherData({ q: `${lat},${lon}`, days: 3, type: "forecast" });
        lastData = data;
        showStatus("現在地の天気を取得しました。");
        renderForecast(data);

      } catch (err) {
        showError(`取得に失敗しました:${err.message}`);

      } finally {
        endLoading();
      }
    },
    () => {
      showError("位置情報の取得に失敗しました。許可設定を確認してください。");
      endLoading();
    }
  );
}
JavaScript

ここでも、
getWeatherData を使うことで
「都市名でも現在地でも同じ書き方」で済んでいます。

検索履歴とお気に入りも「トリガー」として統一する

履歴ボタンやお気に入りボタンは、
最終的にはすべて
fetchForecastByCity を呼ぶ“トリガー”になります。

const history = [];
let favoriteCity = null;

function addHistory(city) {
  if (history.includes(city)) return;
  history.unshift(city);
  if (history.length > 5) history.pop();
  renderHistory();
}

function renderHistory() {
  const historyDiv = document.getElementById("history");

  if (history.length === 0) {
    historyDiv.textContent = "検索履歴はまだありません。";
    return;
  }

  let html = "<p>検索履歴:</p>";
  history.forEach((city) => {
    html += `<button class="history-item" data-city="${city}">${city}</button>`;
  });

  historyDiv.innerHTML = html;

  const buttons = historyDiv.querySelectorAll(".history-item");
  buttons.forEach((btn) => {
    btn.addEventListener("click", () => {
      const city = btn.dataset.city;
      cityInput.value = city;
      fetchForecastByCity(city);
    });
  });
}

function setFavorite(city) {
  favoriteCity = city;
  updateFavoriteUI();
}

function updateFavoriteUI() {
  const favoriteJumpButton = document.getElementById("favoriteJumpButton");
  if (favoriteCity) {
    favoriteJumpButton.disabled = false;
    favoriteJumpButton.textContent = `★ ${favoriteCity} を表示`;
  } else {
    favoriteJumpButton.disabled = true;
    favoriteJumpButton.textContent = "★ お気に入りを表示";
  }
}
JavaScript

お気に入りボタンの動きも、
最終的には fetchForecastByCity を呼ぶだけです。

const favoriteButton = document.getElementById("favoriteButton");
const favoriteJumpButton = document.getElementById("favoriteJumpButton");

favoriteButton.addEventListener("click", () => {
  const city = cityInput.value.trim();
  if (!city) {
    showError("お気に入りに登録するには都市名を入力してください。");
    return;
  }
  setFavorite(city);
  showStatus(`「${city}」をお気に入りに登録しました。`);
});

favoriteJumpButton.addEventListener("click", () => {
  if (!favoriteCity) {
    showError("お気に入りがまだ登録されていません。");
    return;
  }
  cityInput.value = favoriteCity;
  fetchForecastByCity(favoriteCity);
});
JavaScript

ここでの大事な感覚は、
「どこから来ても、最終的には同じ非同期関数を呼ぶ」
という構造です。


エラーハンドリングを「一貫したルール」にする

3 段階のエラーを 1 本のメッセージにまとめる

getWeatherData の中で、

HTTP エラー → throw new Error(HTTPエラー(status))
API エラー → throw new Error(message)

としているので、
UI 側では「err.message」だけを見ればよくなります。

async function fetchForecastByCity(city) {
  const error = validateCity(city);
  if (error) {
    showError(error);
    resultDiv.textContent = "";
    return;
  }

  showLoading();
  startLoading();

  try {
    const data = await getWeatherData({ q: city, days: 3, type: "forecast" });
    lastData = data;
    showStatus("3日分の天気を取得しました。");
    renderForecast(data);
    addHistory(city);

  } catch (err) {
    showError(`取得に失敗しました:${err.message}`);

  } finally {
    endLoading();
  }
}
JavaScript

これで、

ネットワークエラー(fetch 自体の失敗)
HTTP エラー(!response.ok)
API エラー(data.error)

がすべて
catch の中の一行で扱えるようになります。

ここまで来ると、
「エラーハンドリングの型が完全に自分のものになっている」
と言っていいです。


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

7日目の本質は、

fetch・async/await・エラーハンドリングを
「バラバラのテクニック」ではなく
“一つの設計パターン”として捉え直すことです。

API 通信専用の関数(getWeatherData)を作る
UI 側の非同期関数は「状態更新と表示」に集中させる
状態(unit・lastData・favoriteCity・history)を中心にコードを読む
エラーはすべて Error として投げて、UI 側で一括して扱う

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

この 7 日間で身につけた型は、
天気 API だけでなく、
どんな REST API・JSON API にもそのまま使えます。

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