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

APP JavaScript
スポンサーリンク

2日目のゴールと全体像

2日目のテーマは
「1日目で作った“動くニュース検索”を、実用レベルに近づけること」です。

具体的には、こういうところを狙います。

単なる「キーワード検索」から一歩進めて、期間や並び順などの条件を増やすこと。
fetch と async/await の書き方はそのままに、コードを読みやすく整理すること。
エラーハンドリングを“ユーザー目線”で少し丁寧にすること。
ローディング表示を「状態」として扱う感覚をつかむこと。

1日目で作ったものを「書き直す」のではなく、「育てる」イメージで進めます。


1日目のコードを「型」として再確認する

中心となる非同期関数の形

まず、1日目の fetchNews は、すでにかなり良い形をしています。

async function fetchNews(keyword) {
  statusDiv.textContent = "ニュースを取得中です…";
  resultDiv.textContent = "";
  searchButton.disabled = true;

  try {
    const url = buildUrl(keyword);
    const response = await fetch(url);

    if (!response.ok) {
      statusDiv.textContent = `サーバーエラーが発生しました。(${response.status})`;
      return;
    }

    const data = await response.json();

    if (data.status === "error") {
      statusDiv.textContent = `エラー:${data.message}`;
      return;
    }

    statusDiv.textContent = "ニュースの取得に成功しました。";
    renderArticles(data);

  } catch (error) {
    statusDiv.textContent = "通信に失敗しました。ネットワークを確認してください。";
    console.error(error);

  } finally {
    searchButton.disabled = false;
  }
}
JavaScript

ここで、すでに大事なポイントは押さえられています。

非同期処理を async 関数に閉じ込めていること。
await で fetch と json 変換を順番に待っていること。
try の中で「成功時の処理」、catch で「通信エラー」、finally で「後片付け」をしていること。
HTTP レベルのエラー(response.ok)と、API 独自のエラー(data.status === “error”)を分けていること。

2日目は、この「型」を崩さずに、周りを強化していきます。


入力バリデーションを少しだけ“本気”にする

キーワードのチェックを関数に切り出す

1日目では「空文字かどうか」だけをチェックしていました。
2日目では、バリデーションを関数にして、少しだけルールを増やします。

function validateKeyword(keyword) {
  if (!keyword.trim()) {
    return "キーワードを入力してください。";
  }
  if (keyword.length > 50) {
    return "キーワードが長すぎます。50文字以内で入力してください。";
  }
  return null;
}
JavaScript

この関数は「問題があればエラーメッセージを返す」「問題なければ null を返す」という形にしておくと、とても扱いやすくなります。

fetchNews の入り口でバリデーションを使う

async function fetchNews(keyword) {
  const error = validateKeyword(keyword);
  if (error) {
    statusDiv.textContent = error;
    resultDiv.textContent = "";
    return;
  }

  statusDiv.textContent = "ニュースを取得中です…";
  resultDiv.textContent = "";
  searchButton.disabled = true;

  try {
    const url = buildUrl(keyword);
    const response = await fetch(url);
    // 以下は 1日目と同じ流れ
  } catch (error) {
    // 省略
  } finally {
    searchButton.disabled = false;
  }
}
JavaScript

ここでのポイントは、「バリデーションで引っかかったら、そもそも fetch を呼ばない」ということです。
これは API に無駄なリクエストを送らない、という意味でも大事ですし、ユーザーにとっても「何が悪いのか」が明確になります。


NewsAPI のパラメータを増やして“検索条件”を体験する

日付範囲や並び順を UI に追加する

NewsAPI の /everything には、例えばこんなパラメータがあります。

from:いつからのニュースか(例: 2026-01-20)
to:いつまでのニュースか
sortBy:並び順(publishedAt, relevancy, popularity など)

2日目では、次のような UI を足してみましょう。

<input id="keywordInput" placeholder="キーワード (例: JavaScript)" />

<label>
  開始日:
  <input id="fromInput" type="date" />
</label>

<label>
  終了日:
  <input id="toInput" type="date" />
</label>

<select id="sortSelect">
  <option value="publishedAt">新しい順</option>
  <option value="relevancy">関連度順</option>
  <option value="popularity">人気順</option>
</select>

<button id="searchButton">ニュース検索</button>

<div id="status"></div>
<div id="result"></div>

JavaScript 側で要素を取得します。

const keywordInput = document.getElementById("keywordInput");
const fromInput = document.getElementById("fromInput");
const toInput = document.getElementById("toInput");
const sortSelect = document.getElementById("sortSelect");
JavaScript

buildUrl を「オプション付き」にする

1日目の buildUrl はキーワードだけを受け取っていました。
2日目では、オブジェクトを受け取る形にして、柔軟にします。

const API_KEY = "YOUR_API_KEY";
const baseUrl = "https://newsapi.org/v2/everything";

function buildUrl({ keyword, from, to, sortBy }) {
  const params = new URLSearchParams({
    q: keyword,
    apiKey: API_KEY,
    language: "ja",
    pageSize: "10",
    sortBy: sortBy || "publishedAt"
  });

  if (from) {
    params.set("from", from);
  }
  if (to) {
    params.set("to", to);
  }

  return `${baseUrl}?${params.toString()}`;
}
JavaScript

ここで大事なのは、「必須のもの(q, apiKey)は必ず入れる」「任意のもの(from, to)はあれば入れる」という考え方です。

fetchNews から buildUrl を呼ぶときの形

async function fetchNews() {
  const keyword = keywordInput.value.trim();
  const from = fromInput.value;
  const to = toInput.value;
  const sortBy = sortSelect.value;

  const error = validateKeyword(keyword);
  if (error) {
    statusDiv.textContent = error;
    resultDiv.textContent = "";
    return;
  }

  statusDiv.textContent = "ニュースを取得中です…";
  resultDiv.textContent = "";
  searchButton.disabled = true;

  try {
    const url = buildUrl({ keyword, from, to, sortBy });
    const response = await fetch(url);

    if (!response.ok) {
      statusDiv.textContent = `サーバーエラーが発生しました。(${response.status})`;
      return;
    }

    const data = await response.json();

    if (data.status === "error") {
      statusDiv.textContent = `エラー:${data.message}`;
      return;
    }

    statusDiv.textContent = "ニュースの取得に成功しました。";
    renderArticles(data);

  } catch (error) {
    statusDiv.textContent = "通信に失敗しました。ネットワークを確認してください。";
    console.error(error);

  } finally {
    searchButton.disabled = false;
  }
}
JavaScript

ここで、1日目との違いは「URL を作るときに、複数の条件を渡している」だけです。
fetch の書き方、async/await、try-catch-finally の構造はまったく同じです。


ローディング表示を「状態」として扱う感覚をつかむ

isLoading というフラグを導入する

2日目では、ローディングを少しだけ“ちゃんと状態として扱う”ことを意識してみましょう。

let isLoading = false;

function startLoading() {
  isLoading = true;
  statusDiv.textContent = "ニュースを取得中です…";
  resultDiv.textContent = "";
  searchButton.disabled = true;
}

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

そして、fetchNews の中ではこう使います。

async function fetchNews() {
  const keyword = keywordInput.value.trim();
  const from = fromInput.value;
  const to = toInput.value;
  const sortBy = sortSelect.value;

  const error = validateKeyword(keyword);
  if (error) {
    statusDiv.textContent = error;
    resultDiv.textContent = "";
    return;
  }

  startLoading();

  try {
    const url = buildUrl({ keyword, from, to, sortBy });
    const response = await fetch(url);
    // 以下同じ
  } catch (error) {
    // 同じ
  } finally {
    endLoading();
  }
}
JavaScript

「ローディング中かどうか」を変数で持っておくと、
例えば「ローディング中は Enter キーでの再検索を無効にする」など、
後から拡張しやすくなります。


エラーハンドリングを“ユーザー目線”で一段丁寧にする

エラー表示用の小さな関数を作る

1日目では、エラーが起きるたびに statusDiv.textContent = ... と書いていました。
2日目では、少しだけ整理して「意味のある名前の関数」にまとめます。

function showError(message) {
  statusDiv.textContent = message;
  statusDiv.style.color = "red";
}

function showStatus(message) {
  statusDiv.textContent = message;
  statusDiv.style.color = "black";
}
JavaScript

これを使うと、fetchNews はこう書けます。

async function fetchNews() {
  const keyword = keywordInput.value.trim();
  const from = fromInput.value;
  const to = toInput.value;
  const sortBy = sortSelect.value;

  const error = validateKeyword(keyword);
  if (error) {
    showError(error);
    resultDiv.textContent = "";
    return;
  }

  startLoading();
  showStatus("ニュースを取得中です…");

  try {
    const url = buildUrl({ keyword, from, to, sortBy });
    const response = await fetch(url);

    if (!response.ok) {
      showError(`サーバーエラーが発生しました。(${response.status})`);
      return;
    }

    const data = await response.json();

    if (data.status === "error") {
      showError(`エラー:${data.message}`);
      return;
    }

    showStatus("ニュースの取得に成功しました。");
    renderArticles(data);

  } catch (error) {
    showError("通信に失敗しました。ネットワークを確認してください。");
    console.error(error);

  } finally {
    endLoading();
  }
}
JavaScript

ここでのポイントは、「成功メッセージ」と「エラーメッセージ」を視覚的にも区別していることです。
ユーザーにとって「今どういう状態なのか」が、文字だけでなく色でも伝わるようになります。


2日目の完成コード(重要部分をまとめて)

const API_KEY = "YOUR_API_KEY";
const baseUrl = "https://newsapi.org/v2/everything";

const keywordInput = document.getElementById("keywordInput");
const fromInput = document.getElementById("fromInput");
const toInput = document.getElementById("toInput");
const sortSelect = document.getElementById("sortSelect");
const searchButton = document.getElementById("searchButton");
const statusDiv = document.getElementById("status");
const resultDiv = document.getElementById("result");

let isLoading = false;

function validateKeyword(keyword) {
  if (!keyword.trim()) {
    return "キーワードを入力してください。";
  }
  if (keyword.length > 50) {
    return "キーワードが長すぎます。50文字以内で入力してください。";
  }
  return null;
}

function buildUrl({ keyword, from, to, sortBy }) {
  const params = new URLSearchParams({
    q: keyword,
    apiKey: API_KEY,
    language: "ja",
    pageSize: "10",
    sortBy: sortBy || "publishedAt"
  });

  if (from) {
    params.set("from", from);
  }
  if (to) {
    params.set("to", to);
  }

  return `${baseUrl}?${params.toString()}`;
}

function showError(message) {
  statusDiv.textContent = message;
  statusDiv.style.color = "red";
}

function showStatus(message) {
  statusDiv.textContent = message;
  statusDiv.style.color = "black";
}

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

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

function renderArticles(data) {
  const articles = data.articles;

  if (!articles || articles.length === 0) {
    resultDiv.textContent = "該当するニュースが見つかりませんでした。";
    return;
  }

  let html = "";

  articles.forEach((article) => {
    const title = article.title || "タイトルなし";
    const description = article.description || "説明文はありません。";
    const url = article.url;
    const source = article.source?.name || "不明なソース";

    html += `
      <div class="article">
        <h3>${title}</h3>
        <p>${description}</p>
        <p>提供元:${source}</p>
        <p><a href="${url}" target="_blank" rel="noopener noreferrer">記事を読む</a></p>
      </div>
    `;
  });

  resultDiv.innerHTML = html;
}

async function fetchNews() {
  if (isLoading) {
    return;
  }

  const keyword = keywordInput.value.trim();
  const from = fromInput.value;
  const to = toInput.value;
  const sortBy = sortSelect.value;

  const error = validateKeyword(keyword);
  if (error) {
    showError(error);
    resultDiv.textContent = "";
    return;
  }

  startLoading();
  showStatus("ニュースを取得中です…");
  resultDiv.textContent = "";

  try {
    const url = buildUrl({ keyword, from, to, sortBy });
    const response = await fetch(url);

    if (!response.ok) {
      showError(`サーバーエラーが発生しました。(${response.status})`);
      return;
    }

    const data = await response.json();

    if (data.status === "error") {
      showError(`エラー:${data.message}`);
      return;
    }

    showStatus("ニュースの取得に成功しました。");
    renderArticles(data);

  } catch (error) {
    showError("通信に失敗しました。ネットワークを確認してください。");
    console.error(error);

  } finally {
    endLoading();
  }
}

searchButton.addEventListener("click", () => {
  fetchNews();
});
JavaScript

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

2日目で本当に持ち帰ってほしいのは、次の感覚です。

fetch と async/await の「型」はもう変えなくていい。
変わるのは「URL の組み立て方」と「レスポンスの使い方」だけだということ。
ローディングやエラー表示は、“状態”として扱うと設計が楽になること。
バリデーションやエラー表示を関数に切り出すと、コードが一気に読みやすくなること。

天気 API でも、ニュース API でも、
やっていることの本質は同じです。

3日目以降は、このニュースアプリに
ページネーション(次のページのニュース)や
検索履歴、ソース別フィルタなどを足して、
「毎日使えるニュースビューア」に近づけていきましょう。

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