はじめに

fetch は「Web API と通信するための標準手段」です。

現代の Web アプリケーションは、サーバーやクラウド上の Web API と通信してデータを取得・送信する ことが前提になっています。

  • ユーザー情報を取得する
  • 投稿一覧を読み込む
  • ログイン処理を行う
  • 商品データを検索する
  • サーバーにデータを保存する

こうした処理はすべて Web API へのアクセス です。

そして Web API の多くは、データの受け渡しに JSON を使う のが一般的です。

JSON が使われる理由は、

  • 軽量で読み書きしやすい
  • JavaScript と相性が良い(オブジェクトとして扱える)
  • ほぼすべての言語・フレームワークが対応している

つまり、Web API の世界では JSON が事実上の標準フォーマット になっているからです。


ブラウザの Web API アクセスの進化

昔は、Web API にアクセスするために XMLHttpRequest(XHR) を使っていました。しかし、コードが複雑で扱いにくいという問題がありました。

そこで登場したのが fetch API です。

fetch は、

  • シンプル
  • Promise ベース
  • JSON と相性が良い
  • async / await と自然につながる

という特徴を持ち、「Web API と通信するための標準インターフェース」 として設計されています。


fetch の基本

fetch は「Web API にリクエストを送る関数」です。

Promise に対応している為、最も基本的な形は、下記のようになります。

fetch("/api/user")
  .then(res => {
    if (!res.ok) {
      throw new Error(`HTTP Error: ${res.status}`);
    }
    return res.json();
  })
  .then(data => console.log(data))
  .catch(err => console.error(err));

このコードは、「/api/user という Web API にアクセスし、返ってきた JSON を JavaScript のオブジェクトとして扱う」という意味です。

fetchPromise を返すのには、理由があります。それは、Web API へのアクセスは 時間がかかる処理だからです。

  • ネットワーク通信
  • サーバー側の処理
  • レスポンスの受信

これらはすべて非同期で行われるため、fetchPromise(未来の値) を返します。

fetch はレスポンスのヘッダと、ボディを読み込むためのストリームを返します。この際、ボディはまだ読み込まれていないため、JSON として扱うには .json() を呼び出してパースする必要があります。

又、fetch には、 HTTP エラー(404, 500)では reject しないという、仕様上の最大の落とし穴が存在します。そのため、必ず res.ok を確認する必要があります。

この書き方が良い理由は、

  • HTTP エラー
  • ネットワークエラー
  • JSON パースエラー
  • UI 更新エラー

これらを 1 つの catch にまとめられる からです。


fetchasync / await への自然な流れ

fetch の処理は、Promise チェーンでも書けますが、Web API の処理は複雑になりやすいため、async / await の方が圧倒的に読みやすくなります。

async function loadUser() {
  try {
    const res = await fetch("/api/user");
    if (!res.ok) {
      throw new Error(`HTTP Error: ${res.status}`);
    }
    const data = await res.json();
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

loadUser();

👉 このように、「Web API にアクセス → JSON を読む → UI を更新する」という一連の流れが、同期処理のように自然に書けるので、実務では、こちらが使われることが多いです。

非同期処理の書き方については、過去記事、「非同期処理のモダンな書き方」を参照してください。


POST / PUT / DELETE の基本

fetch は データを取得する GET だけでなく、第二引数にオプションを渡すだけで、POST / PUT / DELETE を簡単に扱えます。

POST

await fetch("/api/user", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Kon" })
});

PUT

await fetch("/api/user/1", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Updated" })
});

DELETE

await fetch("/api/user/1", { method: "DELETE" });

CORS の基礎

さて、fetch API により、外部サーバーの Web API にアクセスし、データを取得したり、受け渡しできることがわかりました。

しかし、これだけでは、CORS エラーという予期せぬエラーにみまわれることがあります。

その為、fetch API の利用にあたっては、この CORS という仕組みについて理解しておく必要があります。


CORS とは何か?

CORS(Cross-Origin Resource Sharing)は、「この Web API は、どのサイトからのアクセスを許可するか?」をサーバー側がブラウザに伝える仕組みです。

ブラウザは、サーバーが「このオリジンを許可する」と明示しなかった場合に、セキュリティのため通信をブロックします。これが、「 CORS エラー」です。

では、ブラウザは、なにを基準に判断しているのでしょうか? その為に、使用されるのが、オリジンです。

オリジンとは、以下の 3 つの組み合わせのことをいいます。

  • プロトコル(http / https)
  • ドメイン
  • ポート番号

ブラウザは、現在のサイトが、サーバーが許可した、この 3 つのすべてが一致しているかどうかを判断しています。


図解:CORS の仕組み

[ ブラウザ ]  -- リクエスト -->  [ Web API サーバー ]
      ↑                                |
      |                                |
      └-- サーバーが許可ヘッダを返し、-┘
          ブラウザが安全性を判断する

ブラウザは、現在の頁の JavaScript が fetch を行う際、Web API サーバーに、問い合わせを行います。

例えば、現在の頁のドメインが https://blog.konkitsune.com であった場合、https://api.example.com が提供する Web API に fetch するとします。

その際、オリジンが違うので、ブラウザは Web API サーバーに「このサイトからアクセスしていい?」と確認します。

その応答として、Web API サーバーが「許可するよ」と返すのが Access-Control-Allow-Origin です。

Access-Control-Allow-Origin: https://blog.konkitsune.com

これはブラウザに対して、「このオリジン(https://blog.konkitsune.com)からのアクセスは OK です」という意味になります。

ブラウザはこれを確認して、初めて JavaScript にレスポンスを渡します。これが、CORS の仕組みです。

注)Access-Control-Allow-Origin: * は便利ですが、Access-Control-Allow-Credentials: true と併用できない為、認証情報(Cookie / Authorization ヘッダ)を伴うリクエストでは使用できません。


CORS エラーの正体

CORS エラーは JavaScript のエラーではありません。ブラウザが安全のためにサーバーが許可していない通信をブロックしているのです。

その為、CORS エラーは、残念ながら、クライアント側では解決できません。

つまり、CORS は 100% Web API サーバー側の設定で決まるということです。

👉 CORS エラーを回避するには、Web API サーバーに、現在のオリジンに対する、許可を与えてもらう必要があります。


ブラウザが Web API サーバーに事前に問い合わせる仕組み

ブラウザは、危険な可能性があるリクエストを送る前に、「このリクエストを送っても安全ですか?」と API サーバーに確認します。

この確認を プリフライト(OPTIONS リクエスト) と呼びます。

では、なぜプリフライトが必要なのでしょうか。

理由は ブラウザのセキュリティモデル(Same-Origin Policy) にあります。

ブラウザは、悪意あるサイトが勝手に他のサイトの API を叩くのを防ぐため、オリジンが違うサイトへの “危険な” リクエストを制限するというルールを持っています。

ブラウザは、以下の条件に当てはまると「これは危険かもしれない」と判断します。


プリフライトが発生する条件

下記に、ブラウザが、プリフライトが発生する条件を示します。

① Content-Type が “特殊” な場合

以下以外の Content-Type を使うとプリフライトが発生します:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

例:

Content-Type: application/json

② カスタムヘッダを送る場合

「安全ではない可能性がある」と判断されます。

例:

X-API-Key: 12345
Authorization: Bearer token

③ GET / POST / HEAD 以外のメソッド

これらは、「データを変更する可能性が高い」ため、プリフライトが必要になります。

  • PUT
  • DELETE
  • PATCH

図解:プリフライトの流れ

① ブラウザ
   ↓ OPTIONS(確認)
② API サーバー
   ↓ OK の返事(許可ヘッダ)
③ ブラウザ
   ↓ 本番のリクエスト(GET/POST/PUT/DELETE)
④ API サーバー
   ↓ 本番レスポンス
⑤ ブラウザ → JavaScript に渡す


実際の通信例(PUT / JSON の場合)

あなたが次のような fetch を書いたとします:

fetch("https://api.example.com/user/1", {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Kon" })
});

ブラウザはこう判断します:

「PUT だし JSON だし、これは危険かもしれない。まずサーバーに確認しよう。」

① ブラウザ → サーバー(OPTIONS)

OPTIONS /user/1 HTTP/1.1
Origin: https://blog.konkitsune.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type

② サーバー → ブラウザ(許可の返事)

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://blog.konkitsune.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type

この返事を見てブラウザはこう判断します:

「OK、サーバーが許可してる。本番の PUT リクエストを送っていい。」

③ ブラウザ → サーバー(本番の PUT)

PUT /user/1
Content-Type: application/json
Origin: https://blog.konkitsune.com

{"name":"Kon"}

④ サーバー → ブラウザ(本番レスポンス)

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://blog.konkitsune.com


プリフライトが発生しない(=Simple Request)条件

下記の条件の場合、プリフライトが発生しません。

  • メソッドが GET / POST / HEAD のいずれか
  • Content-Type が以下のいずれか
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • カスタムヘッダを送らない

プリフライトの本質

プリフライトとは、「このリクエストを送っても安全ですか?」とブラウザがサーバーに事前確認する仕組みであり、サーバーが許可しない限り、本番リクエストは送られないという点が最重要です。

よくある誤解

❌ 「プリフライトは JavaScript 側で制御できる」

→ できません。ブラウザの仕様です。

❌ 「fetch のオプションでプリフライトを回避できる」

→ できません。

❌ 「CORS エラーはサーバーではなくクライアントの問題」

→ 100% サーバー側の設定です。

内容 説明
プリフライトとは 本番リクエストの前に送られる “安全確認”
送信方法 OPTIONS メソッド
いつ発生する? JSON、カスタムヘッダ、PUT/DELETE など
誰が判断する? ブラウザ
誰が許可する? サーバー
JavaScript で回避できる? ❌ できない

「プリフライトはキャッシュされる」という重要な仕様

実務では プリフライトが毎回発生すると遅くなる ため、Web API サーバーは以下のヘッダでキャッシュを設定することができます。

Access-Control-Max-Age: 86400

これは プリフライト結果を 24 時間キャッシュする という意味です。


まとめ

  • fetch は Web API と通信するための標準手段
  • JSON は Web API の事実上の標準フォーマット
  • fetchPromise を返し、async / await と相性が良い
  • HTTP エラーは reject されないため res.ok の判断が必須
  • CORS はブラウザのセキュリティ機構で、サーバー側の設定がすべて
  • async / await により、すべてのエラー処理を 1 箇所で扱える