Nextjs:APIルートの保護 / CORSについて
2026年4月19日CORS
CORS(Cross-Origin Resource Sharing)は「保護」というよりも「許可」の為のブラウザの仕組み
CORSはブラウザの機能なのでサーバー側からのリクエストには効果が無い。
ブラウザ(クライアント側)はセキュリティ上の理由から異なるオリジン(他のサイト)へのリクエスト自体は許可するが、レスポンスの読み取りはCORSにより制限されている。
APIを
fetchさせたい側はHTTPヘッダーのCORS設定で任意のドメインを許可することで、そのドメインがクライアントサイドからfetchできるようにさせる。サブドメインはcross-origin に該当するので、APIを提供する場合はCORSによる許可が必要
サーバー側から異なるオリジンへのfetch:
const res = await fetch("https://xxx.com/api/test");
const data = await res.json();
console.log(data, "data");ターミナルにデータが表示される。
クライアント側から異なるオリジンへのfetch:
useEffect(() => {
fetch("https://xxx.com/api/test")
.then((res) => res.json())
.then((data) => {
console.log(data, "data");
});
}, []);has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
ブラウザの検証ツールにCORSでブロックされた旨の表示が出る。
Access-Control-Allow-Origin
next.config.ts :
const nextConfig: NextConfig = {
async headers() {
return [
{
source: "/api/:path*",
headers: [
{
key: "Access-Control-Allow-Origin",
value: 許可するオリジン,
},
],
},
];
},
};
export default nextConfig;middeware(
proxy.ts)やAPI(route.ts)のレスポンスヘッダーでも設定できる。
// API
return new Response(JSON.stringify(data), {
status: 200,
headers: {
"Access-Control-Allow-Origin": allowedOrigin,
"Access-Control-Allow-Methods": "GET",
},
});認証ヘッダー
Authorization: Bearer:
Authorization: <type> <credentials>という形式。HTTP標準のスキーム
api-key:
x-api-key: <credentials>カスタムヘッダー、任意の名前でいい
使い方:
環境変数(
.env)にユニークなキー(credentials)を設定し、APIへのrequest時にheaderにそのキーを含める。API側はキーが合っているか照合し処理の分岐を行う。
例:
.env:
PROTECTION_TEST_KEY="test1234"page.tsx:
export const Page = async () => {
const response = await fetch("/api/protect", {
headers: {
Authorization: `Bearer ${process.env.PROTECTION_TEST_KEY}`,
},
});
if (!response.ok) {
throw new Error("Unauthorized or error");
}
const data = await response.json();
console.log(data);route.ts:
const AUTH_SCHEME = "Bearer";
export async function GET(req: NextRequest) {
const authorization = req.headers.get("authorization");
if (!authorization) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
const [scheme, token] = authorization.split(" ");
if (scheme !== AUTH_SCHEME || !token) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
if (token !== process.env.PROTECTION_TEST_KEY) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}
// DBや認証が関わる処理
}- ここまで行うとサーバー側( Postmanやcurl )からAPIにアクセスすると Unauthorized が返ってくる。つまりサーバー側も保護出来ている。
api-keyのようなカスタムヘッダーの場合もほとんど同じやり方
// route.ts
export async function GET(req: NextRequest) {
if (req.headers.get("api-key") !== process.env.PROTECTION_TEST_KEY) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
return NextResponse.json({ data: "protectData" });
}RateLimit
RateLimitも認証ヘッダーと同じでAPI側に保護ロジックを書くのでサーバー側からも保護できる。
import { headers } from "next/headers";
import { RateLimiterMemory } from "rate-limiter-flexible";
export const getUserIp = async () => {
const headersList = await headers();
const ip =
headersList.get("x-forwarded-for")?.split(",")[0].trim() ||
headersList.get("x-real-ip") ||
headersList.get("cf-connecting-ip") ||
"unknown";
return ip;
};
export const rateLimiter = new RateLimiterMemory({
points: 5,
duration: 60, // 5req/min
});route.ts:
export async function GET(req: NextRequest) {
const userIp = await getUserIp();
try {
await rateLimiter.consume(`routeA:${userIp}`, 1);
return NextResponse.json({
message: "Success",
});
} catch {
return NextResponse.json(
{ error: "Too Many Requests" },
{ status: 429 }
);
}
}rate-limiter-flexibleというライブラリを使った例*注意点:
consume()に渡すキーをroute毎にユニークにしないとlimitの値が共有される。基本的には(upstash)Redisを使った方が良い