Use Cases (更新: 2026/6/2)

Claude CodeでCORS設定完全ガイド:Express・Fastify・Workers・Next.js実装

Claude CodeでCORS設定を安全に実装。preflight、credentials、origin allowlist、検証コマンドまで解説。

Claude CodeでCORS設定完全ガイド:Express・Fastify・Workers・Next.js実装

CORS設定をClaude Codeで正しく実装する

フロントエンドをlocalhost:3000、APIをlocalhost:8787で動かしただけなのに、ブラウザにCORSエラーが出る。この場面で「とりあえずAccess-Control-Allow-Origin: *を付ければよい」と考えると、認証Cookieや管理画面を扱うAPIでは危険です。

CORS(Cross-Origin Resource Sharing)は、異なるオリジンからのブラウザ通信をサーバー側が許可するための仕組みです。オリジンとは、URLのスキーム、ホスト、ポートの組み合わせです。https://app.example.comhttps://api.example.comは別オリジンですし、http://localhost:3000http://localhost:5173も別オリジンです。

この記事では、Claude Codeに丸投げしがちなCORS設定を、初心者でもレビューできる粒度に分解します。Express、Fastify、Cloudflare Workers、Next.js Route Handlerでコピペして動かせる例を置き、preflight、credentials、origin allowlist、テストコマンド、Claude Codeへのレビュー依頼テンプレまでまとめます。

特にMasaが何度も踏んだ落とし穴は、CORSを「認証」や「攻撃防御」だと思い込むことでした。CORSはブラウザがレスポンスを読めるかを制御する仕組みであり、curlやサーバー間通信を止める認可機構ではありません。API側の認証、CSRF対策、レート制限、セキュリティヘッダーとは別に設計してください。

sequenceDiagram
  participant Browser as Browser
  participant API as API server
  Browser->>API: OPTIONS /api/messages<br/>Origin + Access-Control-Request-*
  API-->>Browser: 204 + Access-Control-Allow-*
  Browser->>API: POST /api/messages<br/>Cookie or Authorization
  API-->>Browser: 200 + Access-Control-Allow-Origin

まず決めること

CORS設定を書く前に、次の4点を決めます。ここを曖昧にしたままClaude Codeへ依頼すると、*で全許可するコードや、開発環境のlocalhostを本番に残すコードが出やすくなります。

決める項目注意点
許可するoriginhttps://app.example.comhttps://admin.example.comパスは含めない。末尾スラッシュも不要
認証情報を送るかCookie、Authorization headerCookieを使うならcredentialsSameSite=None; Secureも確認
許可するメソッドGET,POST,PUT,PATCH,DELETE,OPTIONS実際に使うものだけにする
許可するヘッダーContent-Type,Authorization,X-Request-IDpreflightで要求されるヘッダーと一致させる

preflight(プリフライト)は、本番前の確認リクエストです。ブラウザはJSONのPOSTAuthorizationヘッダー付きリクエスト、PUTDELETEなどを送る前に、OPTIONSで「この通信は許可されていますか」とAPIへ聞きます。APIがAccess-Control-Allow-MethodsAccess-Control-Allow-Headersを返さないと、本リクエストは送られません。

ExpressでのCORS設定

Node.js 20以上を想定した最小構成です。cors middlewareの公式ドキュメントでは、originに関数を渡してリクエストごとに判定できます。認証付きリクエストを扱うため、許可リストに一致したoriginだけを反射し、credentials: trueを付けます。

npm init -y
npm install express cors
node server.mjs
// server.mjs
import express from "express";
import cors from "cors";

const app = express();

const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
  "http://localhost:5173",
]);

function isAllowedOrigin(origin) {
  if (!origin) return true;
  if (allowedOrigins.has(origin)) return true;
  return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}

const corsOptions = {
  origin(origin, callback) {
    callback(null, isAllowedOrigin(origin));
  },
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
  exposedHeaders: ["X-Request-ID"],
  maxAge: 86400,
  optionsSuccessStatus: 204,
};

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && !isAllowedOrigin(origin)) {
    return res.status(403).json({ error: "Origin not allowed" });
  }
  next();
});

app.use(cors(corsOptions));
app.use(express.json());

app.get("/api/health", (_req, res) => {
  res.setHeader("X-Request-ID", crypto.randomUUID());
  res.json({ ok: true });
});

app.post("/api/messages", (req, res) => {
  res.setHeader("X-Request-ID", crypto.randomUUID());
  res.json({ ok: true, received: req.body });
});

app.listen(8787, () => {
  console.log("API listening on http://localhost:8787");
});

この例では、開発時だけ任意のlocalhostポートを許可しています。本番ではNODE_ENV=productionを設定し、allowedOriginsに明示したドメインだけを通してください。サーバー間通信のようにOriginヘッダーがないリクエストはCORSの対象外なので許可していますが、APIキーやJWTの認証チェックは別途必要です。

FastifyでのCORS設定

Fastifyでは@fastify/corsを使います。公式READMEにもある通り、originは真偽値、文字列、配列、関数などで指定できます。ただし、正規表現や関数で雑に許可するとDoSや想定外許可の原因になるため、まずはSetで完全一致させるのが堅いです。

npm init -y
npm install fastify @fastify/cors
node server.mjs
// server.mjs
import Fastify from "fastify";
import cors from "@fastify/cors";

const app = Fastify({ logger: true });

const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
  "http://localhost:5173",
]);

function isAllowedOrigin(origin) {
  if (!origin) return true;
  if (allowedOrigins.has(origin)) return true;
  return process.env.NODE_ENV !== "production" && /^http:\/\/localhost:\d+$/.test(origin);
}

app.addHook("onRequest", async (request, reply) => {
  const origin = request.headers.origin;
  if (origin && !isAllowedOrigin(origin)) {
    return reply.code(403).send({ error: "Origin not allowed" });
  }
});

await app.register(cors, {
  origin(origin, callback) {
    callback(null, isAllowedOrigin(origin));
  },
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
  allowedHeaders: ["Content-Type", "Authorization", "X-Request-ID"],
  exposedHeaders: ["X-Request-ID"],
  maxAge: 86400,
  strictPreflight: true,
});

app.get("/api/health", async () => ({ ok: true }));

app.post("/api/messages", async (request) => {
  return { ok: true, received: request.body };
});

await app.listen({ port: 8787, host: "0.0.0.0" });

Fastifyではプラグイン登録順が重要です。認証プラグインや独自hookがOPTIONSを403にしてしまうと、ブラウザは本リクエストを送れません。Claude Codeへ修正を依頼するときは、CORS plugin、認証hook、ルーティングの登録順まで見てもらうと精度が上がります。

Cloudflare WorkersでのCORS設定

Cloudflare WorkersはFetch APIのRequestResponseを直接扱います。OPTIONSを明示的に返し、許可したoriginだけにAccess-Control-Allow-Originを返します。CDNやエッジでoriginごとにレスポンスが変わる場合は、Vary: Originを忘れないでください。

// src/index.ts
const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
]);

function getCorsHeaders(request: Request): HeadersInit | null {
  const origin = request.headers.get("Origin");
  if (!origin) return {};
  if (!allowedOrigins.has(origin)) return null;

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Methods": "GET,POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
    "Access-Control-Max-Age": "86400",
    "Vary": "Origin",
  };
}

export default {
  async fetch(request: Request): Promise<Response> {
    const corsHeaders = getCorsHeaders(request);
    if (corsHeaders === null) {
      return Response.json({ error: "Origin not allowed" }, { status: 403 });
    }

    if (request.method === "OPTIONS") {
      return new Response(null, { status: 204, headers: corsHeaders });
    }

    const url = new URL(request.url);
    if (url.pathname === "/api/messages" && request.method === "POST") {
      const body = await request.json().catch(() => ({}));
      return Response.json({ ok: true, received: body }, { headers: corsHeaders });
    }

    return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
  },
};

Workersでよくある失敗は、通常レスポンスにだけCORSヘッダーを付け、OPTIONSやエラーレスポンスに付け忘れることです。ブラウザのコンソールにはCORSエラーだけが見え、実際には認証エラーやバリデーションエラーだった、という調査が長引きます。

Next.js Route HandlerでのCORS設定

Next.jsのApp Routerでは、APIごとのroute.tsでWeb標準のRequestResponseを使えます。公式ドキュメントにもCORSヘッダーをResponseへ付ける例がありますが、認証付きAPIでは*ではなくallowlistを使います。

// app/api/messages/route.ts
const allowedOrigins = new Set([
  "https://app.example.com",
  "https://admin.example.com",
  "http://localhost:3000",
]);

function getCorsHeaders(request: Request): HeadersInit | null {
  const origin = request.headers.get("Origin");
  if (!origin) return {};
  if (!allowedOrigins.has(origin)) return null;

  return {
    "Access-Control-Allow-Origin": origin,
    "Access-Control-Allow-Credentials": "true",
    "Access-Control-Allow-Methods": "POST,OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type,Authorization,X-Request-ID",
    "Access-Control-Max-Age": "86400",
    "Vary": "Origin",
  };
}

export async function OPTIONS(request: Request) {
  const headers = getCorsHeaders(request);
  if (headers === null) {
    return Response.json({ error: "Origin not allowed" }, { status: 403 });
  }
  return new Response(null, { status: 204, headers });
}

export async function POST(request: Request) {
  const headers = getCorsHeaders(request);
  if (headers === null) {
    return Response.json({ error: "Origin not allowed" }, { status: 403 });
  }

  const body = await request.json().catch(() => ({}));
  return Response.json({ ok: true, received: body }, { headers });
}

next.config.jsheaders()で一括付与する方法もありますが、originごとに出し分ける認証APIではRoute Handler内で判定した方がレビューしやすいです。静的な公開APIだけならheaders()でも十分です。

テストコマンド

ブラウザで失敗してから悩むより、まずcurlでpreflightと本リクエストを分けて確認します。Access-Control-Allow-OriginがリクエストのOriginと完全一致しているか、Access-Control-Allow-Credentials: trueが返っているかを見ます。

curl -i -X OPTIONS http://localhost:8787/api/messages \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization"

curl -i -X POST http://localhost:8787/api/messages \
  -H "Origin: http://localhost:3000" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer dev-token" \
  --data '{"text":"hello"}'

curl -i -X OPTIONS http://localhost:8787/api/messages \
  -H "Origin: https://evil.example" \
  -H "Access-Control-Request-Method: POST"

ブラウザ側の確認は次のように行います。Cookieを送る場合はcredentials: "include"が必要ですが、サーバー側のAccess-Control-Allow-Origin*だとブラウザはレスポンスを読ませません。

await fetch("http://localhost:8787/api/messages", {
  method: "POST",
  credentials: "include",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer dev-token",
  },
  body: JSON.stringify({ text: "hello" }),
});

実例とユースケース

1つ目は、SPAとAPIを別ドメインで運用するケースです。たとえばReactをhttps://app.example.com、APIをhttps://api.example.comに置く場合、ログインCookieを使うならcredentials: true、CookieのSameSite=None; Secure、CSRF対策をセットで確認します。

2つ目は、管理画面だけ追加で許可するケースです。https://admin.example.comをallowlistに入れ、一般ユーザー向けアプリとは別の権限チェックをAPI側で行います。CORSで管理者だけを守れるわけではありません。CORSは「読めるorigin」を制御するだけで、管理者権限は別の認可ロジックで判定します。

3つ目は、Cloudflare WorkersをBFF(Backend for Frontend)や軽いプロキシとして使うケースです。外部APIを直接ブラウザから呼べない場合、Workerで受けてからサーバー側で外部APIへ接続します。このときブラウザに返すWorkerのレスポンスへCORSヘッダーを付ける必要があります。

4つ目は、公開読み取りAPIです。認証情報を送らず、誰に読まれてもよいデータだけならAccess-Control-Allow-Origin: *も選択肢になります。ただし、将来CookieやAuthorizationを追加する予定があるなら、最初からallowlistで設計した方が移行が楽です。

具体的な落とし穴

落とし穴何が起きるか修正
*credentials: trueを併用するブラウザがレスポンスをブロックする明示的なoriginを返す
https://app.example.com/を登録する末尾スラッシュ付きで一致せず失敗originはhttps://app.example.comだけ
localhostだけを許可するポート違いで失敗http://localhost:3000のようにポートまで書く
OPTIONSを認証必須にするpreflightが401/403で止まるpreflightは認証前に処理する
エラーレスポンスにCORSヘッダーがない本当の原因がブラウザに見えない4xx/5xxにも同じCORSヘッダーを付ける
CDNでorigin別レスポンスをキャッシュする別origin向けのヘッダーが混ざるVary: Originを付ける
CORSを認可だと思うcurlやサーバー間通信は止まらない認証・認可・CSRF対策を別に実装する

MDNにも明記されている通り、credentials付きのCORSリクエストに対してAccess-Control-Allow-Origin: *は使えません。Claude Codeがその組み合わせを出したら、その場で修正対象です。

Claude Codeへのレビュー依頼テンプレ

以下のテンプレートは、そのままClaude Codeに貼れます。コードだけでなく、ブラウザのエラー、curl結果、デプロイ先のドメインを一緒に渡すのがコツです。

このリポジトリのCORS設定をレビューしてください。
確認観点:
- credentials付きリクエストでAccess-Control-Allow-Origin: *を使っていないか
- allowlistがscheme/host/portの完全一致になっているか
- OPTIONS preflightが認証middlewareより前に処理されるか
- 4xx/5xxレスポンスにも必要なCORSヘッダーが付くか
- CDNやWorker経由の場合にVary: Originが必要か
修正が必要なら最小差分で提案してください。
次のCORSエラーを原因別に切り分けてください。
ブラウザエラー:
<ここにDevTools Consoleの全文>

curl preflight:
<curl -i -X OPTIONS ... の結果>

期待するorigin:
https://app.example.com

API側の該当ファイルを読んで、再現手順、原因、修正案、追加テストを出してください。
Express/Fastify/Next.js/WorkersのCORS実装を、セキュリティレビュー観点で見てください。
特に次を重点確認:
- originをそのまま反射していないか
- productionでlocalhostが残っていないか
- Authorization headerを許可しているのに認可チェックが不足していないか
- Cookie利用時にSameSite=None; SecureとCSRF対策が説明されているか
- テストコマンドがpreflightと本リクエストを分けているか
結果は「重大」「要修正」「改善提案」に分けてください。

公式リンクと内部リンク

仕様の確認はMDNのCORSガイドを基準にすると迷いにくいです。ExpressならExpress cors middleware、Fastifyなら@fastify/cors、WorkersならCloudflare WorkersのCORS例、Next.jsならRoute HandlersのCORS説明を見てください。Claude Code側の作業テンプレートはClaude Code commandsも参考になります。

関連する実装は、Claude CodeでAPI開発Webセキュリティヘッダー設定Cloudflare Workers活用コードレビュー手順と一緒に読むと、CORSだけを孤立した設定にせずに済みます。

次の一手

CORS設定は、API公開前レビューの最初のチェックポイントに向いています。この記事のコードを自分のドメインへ置き換えたら、Claude Codeセキュリティベストプラクティスも開き、Cookie、CSRF、認可、セキュリティヘッダーまで一度に点検してください。案件や社内プロダクトで使うなら、このチェックリストをレビュー資料にしておくと、追加の実装支援やテンプレート導入の提案にもつなげやすくなります。

この記事で紹介した内容を実際に試した結果

Masaの手元では、Express版とFastify版をlocalhost:8787で起動し、Origin: http://localhost:3000のpreflightとPOSTが成功すること、https://evil.exampleが403になることを確認しました。最も見落としやすかったのは、エラーレスポンスとWorkerのOPTIONSにCORSヘッダーを付け忘れる点でした。Claude Codeには最後に上のレビュー依頼テンプレを渡し、*とcredentialsの併用がないこと、Vary: Originが必要な箇所、localhostが本番に残らないことを再確認させる流れが一番安定しました。

#Claude Code #CORS #セキュリティ #API #Web開発
無料

無料PDF: Claude Code はじめてのチートシート

まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。

スパムは送りません。登録情報は厳重に管理します。

Claude Codeを仕事で使える形にしませんか?

無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。

Masa

この記事を書いた人

Masa

Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。

PR

関連書籍・参考図書

この記事のテーマに関連する書籍を楽天ブックスで探せます。

※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。