CORSの観点からクライアントfetchとサーバサイドfetchを整理する

WEBサービスを作っていると、外部のAPIからデータを取得する場面はよくある。

JavaScriptからデータを取得する基本的なコードはこうだ。

const res = await fetch("https://example.com/api/data.json");
const data = await res.json();

シンプルに見えるが、オリジンをまたいで fetch する場合には「CORS(Cross-Origin Resource Sharing)」という壁を意識する必要がある。

オリジン?

弁当の話ではない。🍱
オリジンとは スキーム + ドメイン + ポート の組み合わせのことだ。

以下のように、少しでも違えば別オリジンと見なされる。

比較同一オリジン?
blog.example.comblog.example.com✅ 同じ
https://http://❌ 別(スキーム違い)
example.comexample2.com❌ 別(ドメイン違い)
blog.example.comexample.com❌ 別(サブドメイン違い)
example.comexample.com:8080❌ 別(ポート違い)

サブドメインが違うだけでも別オリジン扱いになる点は、意外と見落としがちなので気をつけたい。

CORS とは何か

ブラウザには「同一オリジンポリシー(Same-Origin Policy)」というセキュリティルールがある。 これは異なるオリジンへのリクエストを制限する仕組みで、悪意あるサイトが別サイトのAPIを勝手に呼び出すのを防ぐ目的がある。👿

CORS はこの制限を「許可された範囲で緩和する」仕組みだ。 APIサーバーがレスポンスに Access-Control-Allow-Origin ヘッダーを付けることで、ブラウザに「このオリジンからのアクセスは許可している」と伝える。

Access-Control-Allow-Origin: https://example.com

許可するのはサーバー側である

CORS でよく混乱するのが「どちらが許可するのか」という点だ。

これはAPIを提供するサーバー側であり、クライアント(ブラウザ)でどうこうするものではない。

# CORS エラーが発生する構図

ブラウザ(example.com)
   └─fetch("https://api.other.com/data")
       ↑ ブラウザが Origin ヘッダーを付けてリクエスト
       ↑ サーバーの返答に Access-Control-Allow-Origin がなければブロック

逆に言えば、api.other.com 側がヘッダーを返すように設定すれば解決する。 クライアント側でできることは何もない。

一方でサーバーサイドfetchはCORS関係ない

意外に認識が甘いかも、な点としてCORS はあくまでブラウザのセキュリティ機構だ。

故にNode.js などのサーバーサイドコードから fetch する場合、ブラウザを介さないためCORSチェックは発生しない。

# サーバーサイド fetch(CORS 不要)

ブラウザ
  └─ site.example.com のサーバーにリクエスト
       └─ サーバー側コードが fetch する
            └─ api.other.com/data.json
                 ↑ サーバー同士の通信 → ブラウザ不在 → CORS チェックなし

各フレームワークで具体的に見てみる。

Node.js(素のサーバーサイドコード)

// ブラウザを介さないので CORS 不要
const res = await fetch("https://api.other.com/data.json");
const data = await res.json();

Next.js(App Router の Server Component)

// app/page.tsx はサーバーで実行されるため CORS 不要
export default async function Page() {
  const res = await fetch("https://api.other.com/data.json");
  const data = await res.json();
  // ...
}

Pages Router の場合は getServerSideProps を使う。

// pages/index.tsx(Pages Router)
export async function getServerSideProps() {
  const res = await fetch("https://api.other.com/data.json");
  const data = await res.json();
  return { props: { data } };
}

Nuxt.js(Vue.js の SSR フレームワーク)

// server/api/ 配下や useAsyncData のサーバー実行時は CORS 不要
const { data } = await useAsyncData(() =>
  $fetch("https://api.other.com/data.json")
);

※ただし同じ useAsyncData でもクライアントサイドで再実行される場合は CORS が必要になるため、どこで動くコードかを意識することが重要。

いずれも「サーバーで実行されるコード」であれば CORS を気にせず fetch できる。

ついでに、そもそもサーバサイドのfetchになるサーバサイド言語であるPHPとGoがどんな感じになるかもサンプルを載せておく。

PHP

<?php
$response = file_get_contents("https://api.other.com/data.json");
$data = json_decode($response, true);

より実践的には curl を使うことも多い。

<?php
$ch = curl_init("https://api.other.com/data.json");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true);

Go

package main

import (
    "encoding/json"
    "net/http"
)

func main() {
    resp, err := http.Get("https://api.other.com/data.json")
    if err != nil {
        // エラー処理
    }
    defer resp.Body.Close()

    var data interface{}
    json.NewDecoder(resp.Body).Decode(&data)
}

いずれもサーバー上で実行されるコードなので、CORS の設定は一切不要だ。

まとめ:fetch の種類と CORS の関係

fetch の種類CORS が必要か
ブラウザ(クライアントサイド)から別オリジンへ✅ 必要
SSR・Node.js(サーバーサイド)から別オリジンへ❌ 不要
同一オリジンへ❌ 不要

静的サイト(SSG)同士で連携する場合は、ブラウザから直接 fetch することになるので CORS 対応が必須になる。 一方、fetch する側(情報が欲しい側)が SSR であれば、サーバーサイドで fetch できるため CORS を気にせず済む設計が取れる。

ちなみにCORSの読み方は「コルス」や「クロス」と発音される。🦆