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);
}
}
JavaScriptunit と 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 にもそのまま使えます。

