Claude CodeでCORS設定完全ガイド:Express・Fastify・Workers・Next.js実装
Claude CodeでCORS設定を安全に実装。preflight、credentials、origin allowlist、検証コマンドまで解説。
CORS設定をClaude Codeで正しく実装する
フロントエンドをlocalhost:3000、APIをlocalhost:8787で動かしただけなのに、ブラウザにCORSエラーが出る。この場面で「とりあえずAccess-Control-Allow-Origin: *を付ければよい」と考えると、認証Cookieや管理画面を扱うAPIでは危険です。
CORS(Cross-Origin Resource Sharing)は、異なるオリジンからのブラウザ通信をサーバー側が許可するための仕組みです。オリジンとは、URLのスキーム、ホスト、ポートの組み合わせです。https://app.example.comとhttps://api.example.comは別オリジンですし、http://localhost:3000とhttp://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を本番に残すコードが出やすくなります。
| 決める項目 | 例 | 注意点 |
|---|---|---|
| 許可するorigin | https://app.example.com、https://admin.example.com | パスは含めない。末尾スラッシュも不要 |
| 認証情報を送るか | Cookie、Authorization header | Cookieを使うならcredentialsとSameSite=None; Secureも確認 |
| 許可するメソッド | GET,POST,PUT,PATCH,DELETE,OPTIONS | 実際に使うものだけにする |
| 許可するヘッダー | Content-Type,Authorization,X-Request-ID | preflightで要求されるヘッダーと一致させる |
preflight(プリフライト)は、本番前の確認リクエストです。ブラウザはJSONのPOST、Authorizationヘッダー付きリクエスト、PUTやDELETEなどを送る前に、OPTIONSで「この通信は許可されていますか」とAPIへ聞きます。APIがAccess-Control-Allow-MethodsやAccess-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のRequestとResponseを直接扱います。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標準のRequestとResponseを使えます。公式ドキュメントにも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.jsのheaders()で一括付与する方法もありますが、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が本番に残らないことを再確認させる流れが一番安定しました。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
無料PDFで基礎を固めたあと、すぐ使えるテンプレート集で試し、必要なら業務自動化や導入相談まで進められます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
ObsidianメモをCLAUDE.mdに変えるClaude Code運用: 文脈を毎回説明しない仕組み
Obsidianの作業メモからCLAUDE.md用の運用ノートを作り、Claude Codeに安定した文脈を渡す方法。
Claude Code Revenue CTA Routing: 記事からPDF、Gumroad、相談へ送る設計
PVだけで終わらせず、読者の状態に合わせて無料PDF、Gumroad教材、導入相談へ分岐するCTA設計です。
Claude Codeチーム引き継ぎルール: レビュー、権限、収益導線まで渡す実務手順
Claude Codeの作業をチームで渡すための証拠、権限、ロールバック、無料PDF/Gumroad/相談導線の実務ルール。