【Node.js】fetchでAPIを叩く方法|GET・POST・JSON取得とエラー処理の罠

【Node.js】fetchでAPIを叩く方法|GET・POST・JSON取得とエラー処理の罠 Node.js

外部のWeb APIからデータを取得したり、別のサーバーにデータを送ったりするとき、Node.jsでは組み込みのfetchが使えます。Node.js 18以降ではfetchが標準で搭載されているため、node-fetchaxiosといったライブラリをインストールしなくても、ブラウザと同じ感覚でHTTPリクエストを送れます。

注意したいのが、fetchは404や500などのHTTPエラーでも例外(エラー)を投げないことです。ネットワーク自体に失敗したときだけエラーになり、「ページが見つからない」などのレスポンスは正常な結果として返ってきます。これを知らないと、エラーを見逃してしまいます。この記事では、実機のNode.jsで実際にAPIを叩きながら、fetchの使い方を整理します。

先に結論

  • Node.js 18以降はfetchが組み込み。インストール不要です。
  • 基本はconst res = await fetch(url)const data = await res.json()
  • 404や500でも例外は投げられませんres.okres.statusで成否を確認します。
  • POSTはfetch(url, { method, headers, body: JSON.stringify(...) })
  • レスポンスのbody(.json().text())は一度しか読めません
  • ネットワークエラーに備えてtry-catchで囲みます。

逆にAPIサーバーを作る側はExpressで最小のAPIサーバーを作る、低レベルな通信はhttpモジュール、APIキーを環境変数で渡すならprocess完全ガイドもあわせて参考になります。

スポンサーリンク

fetchの基本(GET + JSON)

もっとも基本的な使い方は、await fetch(url)でレスポンスを受け取り、await res.json()でJSONをオブジェクトに変換します。fetchは非同期なのでawaitを付けます。

GETでJSONを取得
// async関数の中、またはトップレベルawaitで実行
const res = await fetch("https://api.github.com/repos/nodejs/node", {
  headers: { "User-Agent": "my-app" },   // GitHub APIはUser-Agentが必要
});

const data = await res.json();   // JSONをオブジェクトに変換

console.log(res.status);    // 200
console.log(data.name);     // node
console.log(data.language); // JavaScript

実機でも、GitHub APIにアクセスしてres.status200data.namenodedata.languageJavaScriptと、JSONが正しく取得・解析できました。fetchはPromiseを返すためawaitで待ち、返ってきたres(Responseオブジェクト)の.json()でボディをJSONとして読み取ります。.json()自体も非同期なのでawaitが必要です。なお、多くのAPIはUser-Agentヘッダーが無いとリクエストを拒否するため、付けておくと安全です。

【最重要】404/500でもthrowしない(res.okを確認)

ここがfetchでもっとも誤解されやすい点です。fetchは、サーバーが404や500を返しても例外を投げません。リクエスト自体は「成功」として扱われ、エラーかどうかはres.ok(200番台ならtrue)やres.statusで自分で確認する必要があります。

res.ok で成否を判定する
const res = await fetch("https://api.github.com/repos/nodejs/not-exist-xyz", {
  headers: { "User-Agent": "my-app" },
});

// 404でも throw されない! ここに来る
console.log(res.status);   // 404
console.log(res.ok);       // false(200番台のときだけ true)

// 必ず res.ok を確認する
if (!res.ok) {
  throw new Error(`APIエラー: ${res.status}`);
}
const data = await res.json();
fetchはHTTPエラーで例外を投げない

実機で確認したところ、存在しないリポジトリにアクセスしてもfetchは例外を投げず、res.status404res.okfalseという結果が普通に返ってきました。つまり、try-catchで囲んでいても、404や500はcatchに入りません。fetchが例外を投げるのは、ネットワークに繋がらない・URLが不正・タイムアウトなど、リクエストが届かなかったときだけです。サーバーが返したエラー(404・500など)を検知するには、必ずres.ok(またはres.status)を自分でチェックしてください。これを忘れると、エラーレスポンスのHTMLやエラーJSONを正常なデータとして処理してしまい、バグの原因になります。axiosなどのライブラリはHTTPエラーで自動的に例外を投げるので、この点がfetchとの大きな違いです。

POSTでJSONを送る

データを送信するPOSTでは、第2引数のオプションでmethodheadersbodyを指定します。JSONを送るときはbodyJSON.stringify()で文字列にし、Content-Typeヘッダーを付けるのがポイントです。

POSTでJSONを送信
const res = await fetch("https://example.com/api/users", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",   // JSONを送る宣言
  },
  body: JSON.stringify({ name: "田中", age: 30 }),   // 文字列にして渡す
});

if (!res.ok) {
  throw new Error(`失敗: ${res.status}`);
}
const result = await res.json();

bodyには文字列を渡すため、オブジェクトをJSON.stringify()でJSON文字列に変換します。そしてContent-Type: application/jsonを付けることで、サーバーに「JSONを送っている」と伝えます。これを忘れると、サーバー側(たとえばExpressのexpress.json())がボディを正しく解析できません。PUTDELETEも同様にmethodを変えるだけで送れます。

レスポンスのbodyは一度だけ

注意点として、レスポンスのボディ(res.json()res.text())は一度しか読めません。同じレスポンスに対して2回読もうとするとエラーになります。

bodyは一度しか読めない
const res = await fetch(url);

const data = await res.json();   // 1回目: OK
// const again = await res.json();  // 2回目: TypeError(bodyは消費済み)

// JSONかテキストか、どちらか一方だけを読む
// const text = await res.text();   // .json() と両方は呼べない
json()とtext()は一度だけ・どちらか一方

実機で確認したところ、同じレスポンスに対してres.json()を2回呼ぶとTypeErrorになりました(ボディは一度読むと消費されるためです)。res.json()res.text()も、両方を呼ぶことはできません。レスポンスがJSONならres.json()、プレーンテキストやHTMLならres.text()、画像などのバイナリならres.arrayBuffer()と、用途に応じてどれか一つを選びます。実機でも、テキストファイルをres.text()で取得すると、ファイルの中身が文字列で得られることを確認しました。レスポンスの中身を複数回使いたいときは、最初に変数へ受け取っておきます。

エラー処理(try-catch + res.ok)

実務では、ネットワークエラー(try-catch)とHTTPエラー(res.ok)の両方に対応します。この2つを組み合わせるのが、fetchの正しいエラー処理です。

正しいエラー処理
async function getUser(id) {
  try {
    const res = await fetch(`https://api.example.com/users/${id}`);

    // HTTPエラー(404・500など)を自分で検知する
    if (!res.ok) {
      throw new Error(`HTTPエラー: ${res.status}`);
    }

    return await res.json();
  } catch (err) {
    // ネットワークエラー or 上で投げたHTTPエラー
    console.error("取得失敗:", err.message);
    return null;
  }
}

このように、try-catchネットワークの失敗を捕まえつつ、if (!res.ok)サーバーが返したエラーを検知してthrowします。投げたエラーは同じcatchでまとめて処理できます。この形をテンプレートとして覚えておくと、fetchを安全に使えます。

ヘッダーと認証トークン

多くのAPIは、認証のためにAPIキーやトークンをヘッダーに付ける必要があります。headersAuthorizationを追加します。キーはコードに直書きせず、環境変数から読み込むのが安全です。

認証トークンを付ける
const token = process.env.API_TOKEN;   // 環境変数から読む(直書きしない)

const res = await fetch("https://api.example.com/me", {
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
});

APIキーやトークンをコードに直接書くと、GitHubなどに公開したときに漏洩します。process.env.API_TOKENのように環境変数から読み込み、キー自体はコードに含めないようにします。Bearer ${token}の形式は、多くのAPIで使われる認証ヘッダーの書き方です。

主なポイントまとめ

fetchの要点をまとめます。

項目 書き方・注意
GET const res = await fetch(url)
JSON取得 const data = await res.json()
成否の確認 res.ok / res.status(404でもthrowしない)
POST { method, headers, body: JSON.stringify(...) }
テキスト取得 await res.text()(jsonと両方は不可)
認証 headers: { Authorization: ... }
エラー処理 try-catchres.okチェック

よくある失敗

404・500をtry-catchで捕まえようとする

fetchはHTTPエラーで例外を投げません。res.okを自分でチェックします。

res.json()を2回呼ぶ

bodyは一度しか読めません。json()text()もどちらか一方だけです。

POSTでJSON.stringifyを忘れる

bodyは文字列です。オブジェクトはJSON.stringify()で変換します。

Content-Typeを付け忘れる

JSONを送るときはContent-Type: application/jsonを付けないと、相手が解析できません。

APIキーをコードに直書きする

漏洩の原因です。process.envなど環境変数から読み込みます。

よくある質問

QNode.jsでfetchを使うのにインストールは必要ですか?
ANode.js 18以降はfetchが標準で組み込まれているため、インストール不要です。node-fetchaxiosを入れなくても、そのままawait fetch(url)でリクエストを送れます。古いバージョンのNode.jsではnode-fetchなどのライブラリが必要です。
Q404エラーがtry-catchで捕まえられません。
AfetchはHTTPエラー(404・500など)では例外を投げない仕様です。ネットワーク自体の失敗だけがcatchに入ります。404などを検知するには、if (!res.ok)res.statusを自分でチェックして、必要ならthrowしてください。
Qres.json()を呼ぶとエラーになります。
A同じレスポンスでres.json()を2回呼んだか、res.json()res.text()を両方呼んだ可能性があります。レスポンスのボディは一度読むと消費されるため、読めるのは一度だけです。中身を複数回使うなら、最初に変数へ受け取っておきます。
QfetchでPOSTするには?
A第2引数に{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(データ) }を渡します。bodyは文字列でなければならないため、オブジェクトはJSON.stringify()でJSON文字列に変換します。
QAPIキーはどこに書けばよいですか?
Aコードに直接書くと、リポジトリを公開したときに漏洩します。process.env.API_TOKENのように環境変数から読み込み、キー自体はコードに含めないでください。.envファイルと環境変数の管理を組み合わせるのが一般的です。

まとめ

  • Node.js 18以降はfetchが組み込みawait fetch(url)await res.json()が基本です。
  • 404・500では例外が投げられないため、res.okで必ず成否を確認します。
  • POSTはmethodheadersJSON.stringifyしたbodyを指定します。
  • レスポンスのbodyは一度だけjson()text()のどちらか一方を使います。
  • エラー処理はtry-catchres.ok、APIキーは環境変数から読み込みます。

Node.jsのfetchは、追加ライブラリなしでAPIを叩ける手軽な手段です。最大の注意点である「HTTPエラーで例外を投げない」さえ押さえれば、安全にデータの取得・送信ができます。res.okのチェックを習慣にして、外部APIとの連携を実装してみてください。