Claude CodeでVercel Edge Functionsを安全に使う実務ガイド
Claude CodeでVercel Edge Runtimeを安全に使う実務ガイド。Middleware、署名検証、A/Bテスト、落とし穴を解説。
Edgeは「速そう」だけで選ばない
Vercel Edge FunctionsやEdge Runtimeは、ユーザーの近くで小さなJavaScriptを実行できる仕組みです。ここでいうEdge Runtimeは、Node.jsそのものではなく、fetch、Request、Response、URL、TextEncoder、Web CryptoのようなWeb標準APIを中心にした実行環境です。初心者向けに言い換えると、「ページやAPIに届く前の軽い関所で、HTTPリクエストを見てすぐ判断する場所」です。
Claude Codeと相性がよいのは、Edgeの実装が単体のアイデアでは終わらないからです。国別リダイレクト、A/Bテスト、軽い認証、署名検証、キャッシュ前処理、Webhook受けを入れると、middleware.ts、app/api/.../route.ts、環境変数、テスト、ログ、Vercelの本番ヘッダーまで同時に見なければなりません。Claude Codeには「このファイルだけ直して」ではなく、「Edge Runtimeで動く制約を守りながら、差分、テスト、ログ漏えいまでレビューして」と頼むほうが実務に近くなります。
ただし、2026年6月時点でVercelの公式ドキュメントは、Edge Runtimeを万能の高速化手段として扱っていません。VercelのEdge Runtime公式ドキュメントでは、Node.jsへの移行推奨やコードサイズ、実行時間、利用可能APIの制約にも触れています。Next.js側もMiddlewareとRoute HandlersをWeb APIベースで説明しています。この記事では「何でもEdgeに寄せる」ではなく、「Edgeに置くと効果が出やすい小さな判断だけを置く」方針で進めます。
関連して、Webhookの基本設計はClaude Code Webhook実装ガイド、速度改善全体はClaude Codeパフォーマンス最適化も合わせて見ると整理しやすいです。
実務で使う5つのユースケース
Edgeに向いている処理は、データベースを深く読まなくても、リクエストのヘッダー、URL、Cookie、短い本文だけで判断できるものです。逆に、複雑な集計、重いORM、長いLLM処理、ファイル処理、社内ネットワークへの直接接続はNode.js側に残すほうが安全です。
| ユースケース | Edgeに置く理由 | Node.js側に残すもの |
|---|---|---|
| 国別リダイレクト | x-vercel-ip-countryなどのヘッダーで入口を即時に振り分けられる | ユーザー設定の永続化、地域別価格の計算 |
| A/Bテスト | Cookieでバケットを決め、ページ到達前にヘッダーへ渡せる | 集計、統計判定、実験停止判断 |
| 軽い認証や署名チェック | 管理画面、プレビュー、Webhookを早い段階で遮断できる | セッション発行、権限管理、監査ログ保存 |
| キャッシュ前処理 | 不要なクエリを正規化し、キャッシュキーを安定させられる | キャッシュの再生成、DB更新、在庫計算 |
| Webhook受け | 小さな本文を検証して内部APIやキューへ渡せる | 重い決済処理、メール送信、リトライ管理 |
Claude Codeに任せるときは、この表をそのまま依頼文に入れるのがおすすめです。「Edgeに置く処理」と「Node.js側に残す処理」を分けておくと、生成されたコードがfs、Buffer、cryptoのNode専用APIに寄ってしまう失敗を減らせます。
flowchart LR
A["User request"] --> B["Next.js Middleware"]
B --> C{"Small decision"}
C --> D["Country redirect"]
C --> E["A/B bucket"]
C --> F["Light auth"]
B --> G["Edge Route Handler"]
G --> H["HMAC signature check"]
H --> I["Internal API or queue"]
この図のポイントは、Edgeを「最終処理の場所」にしないことです。Edgeは入口で分類し、怪しいリクエストを落とし、必要なメタデータを付けて、重い処理は内部APIやキューに渡します。小さく保てるほど、ローカルと本番の差分も追いやすくなります。
Next.js Middlewareのコピー可能な例
次のmiddleware.tsは、国別リダイレクト、A/Bテスト、軽いプレビュー認証、セキュリティヘッダーをまとめた最小構成です。request.geoではなくVercelが付けるヘッダーを使っているのは、ローカル実行やNext.jsのバージョン差分で壊れにくくするためです。ローカルでは国ヘッダーが入らないので、国別分岐は本番またはプレビュー環境で確認します。
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
const PUBLIC_FILE = /\.(?:png|jpg|jpeg|gif|svg|webp|ico|css|js|map|txt)$/i;
const SECRET_HEADER = "x-edge-shared-secret";
export const config = {
matcher: ["/((?!api/webhooks|_next/static|_next/image|favicon.ico).*)"],
};
function chooseBucket(request: NextRequest): "a" | "b" {
const current = request.cookies.get("ab_bucket")?.value;
if (current === "a" || current === "b") return current;
const random = new Uint8Array(1);
crypto.getRandomValues(random);
return random[0] < 128 ? "a" : "b";
}
function localeFromCountry(country: string | null): string | null {
switch (country?.toUpperCase()) {
case "JP":
return "ja";
case "KR":
return "ko";
case "CN":
case "TW":
case "HK":
return "zh";
case "BR":
return "pt";
case "ES":
case "MX":
return "es";
default:
return null;
}
}
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (PUBLIC_FILE.test(pathname)) {
return NextResponse.next();
}
if (pathname === "/") {
const country = request.headers.get("x-vercel-ip-country");
const locale = localeFromCountry(country);
if (locale) {
return NextResponse.redirect(new URL(`/${locale}/`, request.url), 307);
}
}
if (pathname.startsWith("/beta")) {
const bucket = chooseBucket(request);
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-ab-bucket", bucket);
const response = NextResponse.next({
request: { headers: requestHeaders },
});
if (!request.cookies.has("ab_bucket")) {
response.cookies.set("ab_bucket", bucket, {
maxAge: 60 * 60 * 24 * 30,
path: "/",
sameSite: "lax",
secure: request.nextUrl.protocol === "https:",
});
}
return response;
}
if (pathname.startsWith("/preview")) {
const expected = process.env.EDGE_SHARED_SECRET;
const actual = request.headers.get(SECRET_HEADER);
if (!expected || actual !== expected) {
return NextResponse.redirect(new URL("/login", request.url), 307);
}
}
const response = NextResponse.next();
response.headers.set("x-content-type-options", "nosniff");
response.headers.set("referrer-policy", "strict-origin-when-cross-origin");
return response;
}
実務では、Middlewareに機能を詰め込みすぎないことが重要です。上の例でも、A/Bテストはバケットを決めるだけで、結果集計はしていません。プレビュー認証も「共有シークレットがあるか」を見るだけで、ユーザー権限や監査ログは別のサーバー側で扱います。Edgeは全ページの入口を通るため、1つのミスがサイト全体の遅延やリダイレクトループにつながります。
Edge Route HandlerでWebhook署名を検証する
次はapp/api/webhooks/provider/route.tsの例です。HMACは「共有した秘密鍵と本文から作る改ざん検知用の署名」です。Edge RuntimeにはNode.jsのcrypto.createHmacやBufferがない前提で、Web CryptoとTextEncoderだけを使います。署名検証後は、重い処理をEdgeで続けず、内部APIやキューへ渡します。
// app/api/webhooks/provider/route.ts
export const runtime = "edge";
export const preferredRegion = ["iad1", "hnd1"];
const MAX_BODY_BYTES = 256_000;
function hexToBytes(hex: string): Uint8Array {
const clean = hex.replace(/^sha256=/, "").trim();
if (!/^[0-9a-f]+$/i.test(clean) || clean.length % 2 !== 0) {
return new Uint8Array();
}
const bytes = new Uint8Array(clean.length / 2);
for (let index = 0; index < clean.length; index += 2) {
bytes[index / 2] = Number.parseInt(clean.slice(index, index + 2), 16);
}
return bytes;
}
async function hmacSha256(secret: string, payload: string): Promise<Uint8Array> {
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
"raw",
encoder.encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
return new Uint8Array(signature);
}
function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean {
if (a.length !== b.length) return false;
let diff = 0;
for (let index = 0; index < a.length; index += 1) {
diff |= a[index] ^ b[index];
}
return diff === 0;
}
export async function POST(request: Request) {
const secret = process.env.WEBHOOK_SECRET;
const internalOrigin = process.env.INTERNAL_API_ORIGIN;
const internalToken = process.env.INTERNAL_API_TOKEN;
if (!secret || !internalOrigin || !internalToken) {
return Response.json({ error: "server is not configured" }, { status: 500 });
}
const contentLength = Number(request.headers.get("content-length") ?? "0");
if (contentLength > MAX_BODY_BYTES) {
return Response.json({ error: "payload too large" }, { status: 413 });
}
const rawBody = await request.text();
const rawBodyBytes = new TextEncoder().encode(rawBody);
if (rawBodyBytes.byteLength > MAX_BODY_BYTES) {
return Response.json({ error: "payload too large" }, { status: 413 });
}
const provided = hexToBytes(request.headers.get("x-signature-sha256") ?? "");
const expected = await hmacSha256(secret, rawBody);
if (!constantTimeEqual(provided, expected)) {
return Response.json({ error: "invalid signature" }, { status: 401 });
}
const event = JSON.parse(rawBody) as { id?: string; type?: string };
if (!event.id || !event.type) {
return Response.json({ error: "invalid event" }, { status: 400 });
}
await fetch(`${internalOrigin}/api/webhook-events`, {
method: "POST",
headers: {
authorization: `Bearer ${internalToken}`,
"content-type": "application/json",
},
body: JSON.stringify({
id: event.id,
type: event.type,
receivedAt: new Date().toISOString(),
}),
});
return Response.json({ ok: true });
}
ここでの設計判断は3つあります。1つ目は本文サイズを先に見ることです。Edgeは大きな本文や長いストリーミング処理に向きません。2つ目は署名検証前にJSONをパースしないことです。プロバイダーが署名した「生の本文」を使わないと検証がずれます。3つ目は、検証後に内部APIへ渡すだけにしていることです。決済確定、メール送信、CRM更新のような副作用は、再試行や監査がしやすいNode.js側で扱います。
Claude Codeへのレビュー指示と最小テスト
Claude Codeには、機能追加だけでなく「Edge Runtimeで壊れる依存を探す」仕事を明示します。以下のように、見るべきファイル、禁止API、検証コマンド、ログの扱いを指定すると、レビューの粒度が安定します。
Review this Next.js Edge implementation.
Scope:
- middleware.ts
- app/api/webhooks/provider/route.ts
- related tests and environment variable names
Check:
- no Node-only APIs such as fs, net, tls, Buffer, or node:crypto in Edge files
- no direct database connection from Edge Runtime
- country redirect does not loop
- A/B bucket is stable by cookie and not written on every request
- webhook verifies the raw body before JSON parsing
- secrets, signatures, cookies, and authorization headers are not logged
- body size and production-only Vercel headers are documented
Return blockers first, then suggested tests.
ローカル確認では、Edge内ではなく「署名を作る補助スクリプト」としてNode.jsを使ってかまいません。次の手順は最小の動作確認です。
npm run lint
npm run build
vercel dev
BODY='{"id":"evt_123","type":"checkout.completed"}'
SIG=$(node -e "const crypto=require('crypto'); const body=process.argv[1]; console.log('sha256='+crypto.createHmac('sha256', process.env.WEBHOOK_SECRET).update(body).digest('hex'))" "$BODY")
curl -i http://localhost:3000/api/webhooks/provider \
-X POST \
-H "content-type: application/json" \
-H "x-signature-sha256: $SIG" \
--data "$BODY"
curl -I http://localhost:3000/beta
curl -I http://localhost:3000/preview
本番確認では、国別ヘッダー、Cookie、リダイレクト、ログをVercelのPreview Deploymentで見ます。vercel devだけではx-vercel-ip-countryのようなヘッダーやリージョン挙動を完全には再現できません。Claude Codeに「ローカルで確認できること」と「Preview Deploymentでしか確認できないこと」を分けてレシート化させると、公開前レビューがかなり楽になります。
よくある落とし穴
1つ目は、Node.jsのAPIを無意識に使うことです。fs、path、crypto.createHmac、Buffer、TCP接続を前提にするDBクライアントはEdgeで動かないか、ビルド時には気づきにくい失敗になります。署名検証はWeb Cryptoで書く、DBはHTTP APIやキューを挟む、と先に決めます。
2つ目は、データベース接続をEdgeから直接持ちすぎることです。ユーザーに近い場所で処理しても、DBが東京や米国東部の1リージョンにあれば、結局そこへ往復します。接続数も増えます。Edgeでやるのは、キャッシュキー作成、署名検証、不要リクエストの遮断までにして、重い読み書きはNode.js Functionや専用APIに渡すほうが運用しやすいです。
3つ目は、cold startやregionを誤解することです。Edgeなら常にゼロ遅延、世界中で同じ速さ、という理解は危険です。Vercelは近いリージョンで実行できますが、依存するデータソースが遠ければ遅くなります。preferredRegionを指定しても、何がどこで動くかをログと計測で確認する必要があります。
4つ目は、secretやログ漏えいです。Webhook本文、署名、Cookie、Authorizationヘッダーをconsole.logすると、Previewログや外部ログ基盤に残ります。Claude Codeには「デバッグログを入れて」ではなく、「機密値をマスクしたログだけ入れて」と指示します。
5つ目は、body sizeとstreamingです。Edge Route Handlerは軽いリクエストには便利ですが、大きなCSV、画像アップロード、長いLLMストリームを受ける場所ではありません。署名検証用にrequest.text()を読む場合も、本文サイズを上限で切ります。
6つ目は、ローカルと本番の差分です。ローカルではVercelの国別ヘッダーや実際のEdgeリージョン、Previewログ、Cookieのsecure挙動が違います。テストはnpm run build、vercel dev、Preview Deploymentの3段階に分けます。
相談導線とチーム運用
個人開発なら、この記事のMiddlewareとRoute Handlerを小さく試すだけで十分です。チーム導入では、誰がEdgeにコードを置いてよいか、どのAPIを禁止するか、環境変数名をどう管理するか、Previewで誰が確認するかをCLAUDE.mdやレビュー指示に落とし込む必要があります。
ClaudeCodeLabでは、Claude Codeの導入、CLAUDE.md整備、Vercel Edge Runtimeのレビュー観点、WebhookやA/Bテストの検証レシート作りをClaude Code研修・導入相談で扱っています。Edgeは便利ですが、認証、決済、地域ルーティングに絡むため、生成したコードをそのまま本番に出すより、チームの運用ルールまで決めたほうが事故を減らせます。
内部リンクとしては、Webhookの再試行や冪等性を固めたい場合はClaude Code Webhook実装ガイド、速度とキャッシュを広く見たい場合はClaude Codeキャッシュ戦略が次の読み物になります。
実際に試した結果
この記事の構成で、Middlewareはリダイレクト、A/Bバケット、軽い認証だけに絞り、Webhookは署名検証後に内部APIへ渡す形で検証しました。作業してみると、いちばん効いたのはコード量の削減ではなく「Edgeでやらないこと」を先に決めた点でした。Claude Codeにレビュー指示を渡すと、Bufferやnode:cryptoの混入、署名検証前のJSONパース、Previewでしか確認できない国別ヘッダーの見落としを指摘させやすくなりました。Edge Functionsは魔法の高速化ではありませんが、入口の判断を小さく保てば、Vercel上で安全に使える実務部品になります。
無料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/相談導線の実務ルール。