CORSエラーの原因と解決方法:フロントエンドからAPIを叩くときのつまずきポイント
目次
CORSエラーとは何か
フロントエンド開発でほぼ必ず一度は遭遇するのがこのエラーです。
Access to fetch at 'http://localhost:3001/api/users' from origin
'http://localhost:5173' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
CORS(Cross-Origin Resource Sharing)は、ブラウザのセキュリティ機能です。この仕組みを理解せずに対処法だけ覚えると、本番環境で同じ問題を繰り返すことになります。
Same-Origin Policy(同一オリジンポリシー)
CORSエラーの根本にあるのがSame-Origin Policyです。ブラウザは「異なるオリジンへのリクエスト」を制限する仕組みを持っています。
オリジンとは「スキーム + ホスト + ポート番号」の組み合わせです。
| URL | オリジン |
|---|---|
http://localhost:5173 | http://localhost:5173 |
http://localhost:3001 | http://localhost:3001 ← 別オリジン(ポートが違う) |
https://example.com | https://example.com |
https://api.example.com | https://api.example.com ← 別オリジン(サブドメインが違う) |
ローカル開発で「Viteのdev server(port 5173)からExpressのAPIサーバー(port 3001)にfetchしたら怒られた」というのが典型的なCORSエラーのシチュエーションです。
なぜこのような制限があるのか?
悪意のあるサイト(evil.com)が、あなたのSNSのCookieを使って勝手にAPIリクエストを送る「CSRF攻撃」を防ぐためです。Same-Origin Policyがなければ、あなたがログイン中のSNSのデータを別サイトのJavaScriptが盗み読みできてしまいます。
プリフライトリクエスト(Preflight Request)
GETやPOST(Content-Type: application/x-www-form-urlencoded)は「シンプルリクエスト」として直接送信されますが、以下の条件に該当するリクエストは本番リクエストの前にOPTIONSメソッドによる「プリフライトリクエスト」が自動で送られます。
Content-Type: application/json(JSONを送るPOSTリクエスト)PUT/DELETE/PATCHメソッド- カスタムヘッダーを付与する場合(
Authorizationなど)
[ブラウザ] --OPTIONS--> [APIサーバー]
「このオリジンからJSON付きのPOSTを送っていいですか?」
[APIサーバー] --200 OK + CORSヘッダー--> [ブラウザ]
「いいですよ。許可するオリジンとメソッドはこちらです。」
[ブラウザ] --POST(本番リクエスト)--> [APIサーバー]
「では本番データを送ります。」
サーバーがOPTIONSリクエストを適切に処理しないと、本番リクエスト前に詰まってCORSエラーになります。
解決方法1:Expressサーバーにcorsミドルウェアを追加
バックエンドがExpressの場合、corsパッケージを使うのが最もシンプルな解決策です。
npm install cors
// server.js
import express from 'express';
import cors from 'cors';
const app = express();
// ❌ これはダメ:全オリジンを許可(本番では使わない)
// app.use(cors());
// ✅ 特定のオリジンのみ許可
app.use(cors({
origin: ['http://localhost:5173', 'https://your-app.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // Cookie/Authorizationヘッダーを許可する場合
}));
app.use(express.json());
app.get('/api/users', (req, res) => {
res.json([{ id: 1, name: '田中太郎' }]);
});
app.listen(3001, () => console.log('API server running on port 3001'));
credentials: trueを使う場合の注意点: originに*(全オリジン許可)は使えません。必ず具体的なオリジンを指定する必要があります。
解決方法2:Viteのプロキシを使う(開発環境)
開発時は、Viteのプロキシ機能でCORSを回避するのが手軽です。ブラウザからはViteのサーバーにアクセスしているように見えるため、Same-Origin Policyに引っかかりません。
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
server: {
proxy: {
// /api で始まるパスをAPIサーバーに転送
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
// パスを書き換える場合(/api/users → /users)
// rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
このプロキシ設定後は、フロントエンドのfetchをこう書けます。
// フロントエンド
// ❌ 直接バックエンドのポートを指定しない
// const res = await fetch('http://localhost:3001/api/users');
// ✅ 同じオリジン(Vite)に向けてfetch → プロキシが転送
const res = await fetch('/api/users');
const users = await res.json();
解決方法3:CORSヘッダーを手動で設定する
corsパッケージを使わずにヘッダーを手書きする場合はこうなります。
// Express(手動でヘッダーを設定)
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5173');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
// OPTIONSメソッド(プリフライト)に即レスポンスを返す
if (req.method === 'OPTIONS') {
return res.status(204).end();
}
next();
});
よくある落とし穴
Cookie(credentials)を送りたいのにエラーになる
fetchでCookieやAuthorizationヘッダーを含める場合は、クライアント側でもcredentials: 'include'の指定が必要です。
// ❌ デフォルトではCookieを送らない
const res = await fetch('http://localhost:3001/api/me');
// ✅ credentialsを明示的に指定
const res = await fetch('http://localhost:3001/api/me', {
credentials: 'include',
});
この場合サーバー側もAccess-Control-Allow-Credentials: trueかつAccess-Control-Allow-Originにワイルドカード(*)不可というセットの条件になります。
本番環境でだけCORSエラーが出る
開発時にViteプロキシで解決していた場合、本番ビルドではプロキシが効きません。本番用の適切なNginxリバースプロキシ設定か、バックエンド側のCORSヘッダー設定が必要です。
# nginx.conf(リバースプロキシの例)
location /api {
proxy_pass http://localhost:3001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
ブラウザのキャッシュが原因のことも
OPTIONSレスポンスはAccess-Control-Max-Ageヘッダーでキャッシュされます。設定変更後もエラーが続く場合はブラウザのキャッシュをクリアするか、シークレットモードで確認しましょう。
プロを目指す人のためのTypeScript入門
TypeScriptの型システムをしっかり学べる定番書。fetch APIの型安全な使い方やエラーハンドリングも丁寧に解説されています。
※ アフィリエイトリンクを含みます
まとめ
CORSエラーを解決する方法をまとめます。
| シチュエーション | 解決策 |
|---|---|
| バックエンドを自分で管理している | corsミドルウェアで許可オリジンを設定 |
| 開発環境のみ解決したい | Viteのserver.proxyを使う |
| 外部APIでバックエンドを変更できない | 自前のプロキシサーバーを経由させる |
| サードパーティAPIキーを隠したい | バックエンドを中継サーバーにする |
根本的な解決はサーバー側のCORSヘッダー設定です。Access-Control-Allow-Originに適切なオリジンを指定し、本番環境では絶対に*(全許可)にしないよう注意しましょう。