Claude Code and Vercel Edge Functions: Practical Runtime Guide
Use Claude Code with Vercel Edge Runtime: Middleware, signed webhooks, A/B tests, cache prep, and production pitfalls.
Do not choose Edge just because it sounds fast
Vercel Edge Functions run JavaScript in the Edge Runtime, a small Web API based runtime close to incoming traffic. In plain terms, it is a lightweight request gate: it can inspect headers, URLs, cookies, and a small body before the request reaches a page or a heavier API handler. It is not a normal Node.js process. You should expect Web APIs such as fetch, Request, Response, URL, TextEncoder, and Web Crypto, not fs, TCP sockets, Buffer, or node:crypto.
That difference is exactly why Claude Code is useful. A real Edge change is rarely one isolated file. A country redirect touches middleware.ts and deployment headers. A/B testing touches cookies, request headers, analytics, and rollback. A signed webhook touches raw body handling, environment variables, body limits, and downstream queueing. Claude Code can read those files together and review the runtime boundary if you give it a concrete instruction instead of asking for “an Edge function” in the abstract.
As of June 2026, Vercel’s own Edge Runtime documentation is careful about the tradeoffs. It documents supported APIs, code size limits, regions, streaming timing, and even recommends moving some workloads back to Node.js for performance and reliability. The current Next.js docs also describe Middleware and Route Handlers around the Web Request and Response APIs. The practical conclusion is not “move everything to Edge”; it is “put small request decisions at the edge and keep durable work somewhere easier to operate.”
For adjacent ClaudeCodeLab material, pair this article with the Claude Code webhook implementation guide and the Claude Code performance optimization guide. They cover retry behavior, idempotency, caching, and measurement beyond the narrow runtime examples here.
Five practical use cases
Edge is strongest when the answer can be derived from request metadata or a small signed payload. It is weak when the task needs a large dependency tree, a long database transaction, a private network connection, a large upload, or a long-running LLM stream. Use the boundary deliberately.
| Use case | Why it fits Edge | Keep this in Node.js or a backend service |
|---|---|---|
| Country redirect | Vercel can add country headers before app code runs | Persisted user preferences, pricing rules, account policy |
| A/B testing | A cookie can assign a stable bucket before rendering | Analytics aggregation, experiment analysis, rollout decisions |
| Light auth or signature checks | Bad preview or webhook traffic can be rejected early | Session issuing, role management, audit log storage |
| Cache preprocessing | URL and query normalization can stabilize cache keys | Revalidation jobs, inventory updates, expensive recomputation |
| Webhook receiving | A small raw body can be verified and forwarded | Payment finalization, emails, retries, CRM updates |
This table is also a good prompt input for Claude Code. Tell it which work belongs in Edge and which work must remain outside. That prevents the common failure where generated code pulls in a Node-only database client, logs a secret while debugging, or tries to run a full business transaction from middleware.
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"]
The diagram keeps Edge in the role of a gate, not a complete application backend. The middleware classifies traffic and adds stable metadata. The route handler verifies a small webhook and forwards the event. The durable system, whether a Node.js function, queue, workflow worker, or database-backed service, owns side effects and retries.
Copyable Next.js Middleware
The following middleware.ts combines country redirects, A/B bucket assignment, a small preview gate, and response security headers. It uses Vercel request headers rather than request.geo so the behavior is easier to reason about across Next.js versions and local development. Local development usually will not include x-vercel-ip-country, so validate that part on a Vercel Preview Deployment.
// 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;
}
There are several intentional limits in this sample. The A/B test only assigns a bucket; it does not decide a winner. The preview gate checks a shared secret; it does not become your full auth system. The country redirect only runs at /; it does not rewrite every route and risk an infinite loop. These boundaries matter because middleware can run before many routes and a mistake can affect the entire site.
Edge Route Handler for signed webhooks
This app/api/webhooks/provider/route.ts example verifies an HMAC signature using Web Crypto. HMAC means the sender and receiver both know a secret, and the receiver can prove the body was not changed by recomputing the signature. In Edge Runtime we avoid crypto.createHmac and Buffer, then forward the verified event to an internal API that can handle durable work.
// 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 });
}
The ordering is the important part. Check the size before doing expensive work. Read the raw body once. Verify the signature before parsing JSON. Forward only the fields your internal system needs. If the provider retries, your downstream service should still be idempotent, because the Edge handler is only the gate.
Claude Code review prompt and minimal tests
Give Claude Code the runtime rules directly. A vague request like “make this work on Vercel Edge” often produces a plausible implementation but misses logs, body limits, or production-only headers. Use a review prompt like this:
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.
The test helper below uses Node.js only to generate a local HMAC signature. That is fine because it runs outside the Edge function. The Edge code itself still uses Web Crypto.
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
For production confidence, split the receipt into local proof and preview proof. Local proof covers lint, build, route reachability, signature success, and signature failure. Preview proof covers Vercel headers, HTTPS cookies, redirect loops, function logs, and region assumptions. Claude Code can maintain that receipt in the pull request description so reviewers do not have to rediscover the runtime limits every time.
Pitfalls that cause real incidents
The first pitfall is accidental Node.js usage. fs, path, Buffer, crypto.createHmac, native modules, and TCP-oriented database clients do not belong in Edge files. Sometimes the direct import is obvious. Sometimes a helper library imports them indirectly. Ask Claude Code to inspect the dependency path, not only the file you edited.
The second pitfall is keeping too many database connections at the edge. Running near the user does not help if every request then travels to one database region. It can also increase connection pressure. Prefer HTTP APIs, managed data APIs, queues, or a Node.js function near the database when the operation needs durable state.
The third pitfall is misunderstanding cold start and region behavior. Edge can reduce request-gate latency, but it does not make remote data local. preferredRegion is useful when your data source lives in a known region, but you still need logs and measurements to prove the request is taking the path you expect.
The fourth pitfall is leaking secrets through logs. Webhook bodies, signatures, cookies, Authorization headers, and preview secrets should not be printed. Debugging at the edge is tempting because logs are easy to view in Vercel, but that is also why sensitive values spread quickly.
The fifth pitfall is body size and streaming. Edge Route Handlers are useful for small signed requests and fast decisions. Large uploads, CSV ingestion, image processing, and long LLM streams are better handled by infrastructure designed for those workloads. If you must read request.text() for signature verification, enforce a size limit.
The sixth pitfall is local versus production drift. vercel dev is useful, but it does not fully reproduce country headers, deployment regions, preview log behavior, or secure cookie behavior. Treat local tests as necessary but not sufficient.
ClaudeCodeLab CTA and team workflow
For a solo project, the samples above are enough to build a small proof of concept. For a team, the harder problem is making the boundary repeatable: which files can use Edge Runtime, which APIs are forbidden, how environment variables are named, who verifies Preview Deployments, and how generated code gets reviewed before touching auth, payments, or routing.
ClaudeCodeLab helps teams turn that into practical Claude Code rules, CLAUDE.md guidance, Edge Runtime review prompts, webhook verification receipts, and Vercel deployment checks. If you want this adapted to a real repository, the Claude Code training and consultation page is the natural next step. The goal is not to add ceremony; it is to prevent a small middleware change from becoming a site-wide production problem.
Useful internal follow-ups are the Claude Code webhook implementation guide for retries and idempotency, and the Claude Code caching strategies guide for cache-key design.
What happened when I tried it
Using this structure, the biggest improvement was not raw speed. It was clarity. Middleware stayed limited to redirecting, assigning an A/B bucket, adding headers, and rejecting obvious preview traffic. The Edge Route Handler verified a small signed webhook and forwarded it rather than becoming the payment processor. The Claude Code review prompt caught the kinds of mistakes that often slip through: Buffer sneaking into signature code, JSON parsing before raw-body verification, country headers that only exist on Vercel, and logs that were too detailed. Edge Functions are not magic acceleration, but they are useful production tools when the request boundary is kept small and testable.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.