
Edge Computing Patterns
- 15 installs
- 213 repo stars
- Updated August 4, 2026
- yonatangross/orchestkit
Helps with ai & agent building tasks.
About
edge-computing-patterns is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- edge-computing-patterns
- AI & Agent Building
- AI-coding skill
Edge Computing Patterns by the numbers
- 15 all-time installs (skills.sh)
- Ranked #11,187 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yonatangross/orchestkit --skill edge-computing-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 213 |
| Last updated | August 4, 2026 |
| Repository | yonatangross/orchestkit ↗ |
What it does
Helps with ai & agent building tasks.
Files
Edge Computing Patterns
Overview
Edge computing runs code closer to users worldwide, reducing latency from seconds to milliseconds. This skill covers Cloudflare Workers, Vercel Edge Functions, and Deno Deploy patterns for building globally distributed applications.
When to use this skill:
- Global applications requiring <50ms latency
- Authentication/authorization at the edge
- A/B testing and feature flags
- Geo-routing and localization
- API rate limiting and DDoS protection
- Transforming responses (image optimization, HTML rewriting)
Platform Comparison
| Feature | Cloudflare Workers | Vercel Edge | Deno Deploy |
|---|---|---|---|
| Cold Start | <1ms | <10ms | <10ms |
| Locations | 300+ | 100+ | 35+ |
| Runtime | V8 Isolates | V8 Isolates | Deno |
| Max Duration | 30s (paid: unlimited) | 25s | 50ms-5min |
| Free Tier | 100k req/day | 100k req/month | 100k req/month |
Platform-Specific Implementation
For detailed code examples and patterns, load the appropriate reference file:
Cloudflare Workers
Reference: references/cloudflare-workers.md
- Worker fetch handlers and routing
- KV storage patterns (eventually consistent)
- Durable Objects for stateful edge
- Wrangler CLI and wrangler.toml configuration
- Caching strategies with Cache API
Vercel Edge Functions
Reference: references/vercel-edge.md
- Edge Middleware for Next.js (auth, A/B testing, geo-routing)
- Edge API routes with streaming
- Edge Config for feature flags
- Geolocation-based routing patterns
Runtime Differences
Reference: references/runtime-differences.md
- Node.js APIs NOT available at edge
- Web API compatibility matrix
- Polyfill strategies for crypto, Buffer, streams
Edge Runtime Constraints
Available APIs:
- fetch, Request, Response, Headers
- URL, URLSearchParams
- TextEncoder, TextDecoder
- ReadableStream, WritableStream
- crypto, SubtleCrypto (Web Crypto API)
- Web APIs (atob, btoa, setTimeout, etc.)
NOT Available:
- Node.js APIs (fs, path, child_process)
- Native modules and binary dependencies
- File system access
- Some npm packages with Node.js dependencies
Common Patterns Summary
Authentication at Edge
Verify JWT tokens at edge for sub-millisecond auth checks. See references/cloudflare-workers.md for implementation.
Rate Limiting
Use KV (Cloudflare) or Edge Config (Vercel) for distributed rate limiting. Pattern: IP-based key with TTL expiration.
Edge Caching
Cache API with cache-aside pattern. Check cache first, fetch origin on miss, store with TTL.
A/B Testing
Assign users to buckets via cookie, rewrite URLs to variant pages. See references/vercel-edge.md for middleware pattern.
Geo-Routing
Access request.cf.country (Cloudflare) or request.geo (Vercel) for location-based routing.
Best Practices
- Keep bundles small (<1MB compressed)
- Use streaming for large responses to avoid timeouts
- Leverage platform caching (KV, Durable Objects, Edge Config)
- Handle errors gracefully (edge errors cannot be recovered)
- Test cold starts and warm starts separately
- Monitor edge function performance and error rates
- Use environment variables for secrets (never hardcode)
- Implement proper CORS headers for cross-origin requests
Decision Guide
| Use Case | Recommended Platform |
|---|---|
| Global CDN + compute | Cloudflare Workers |
| Next.js middleware | Vercel Edge |
| TypeScript-first | Deno Deploy |
| Stateful edge | Cloudflare Durable Objects |
| Feature flags | Vercel Edge Config |
| Real-time collaboration | Cloudflare Durable Objects + WebSockets |
Resources
Related Skills
caching-strategies- Redis caching patterns that complement edge KV storage and CDN cachingreact-server-components-framework- Next.js App Router patterns for edge-rendered React componentsstreaming-api-patterns- SSE and streaming responses for edge function outputapi-design-framework- REST API patterns for edge-deployed endpoints
Key Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Primary Runtime | V8 Isolates | Sub-millisecond cold starts, security isolation |
| State Management | KV / Edge Config | Eventually consistent, globally replicated |
| Stateful Workloads | Durable Objects | Strong consistency when needed |
| Auth Strategy | JWT at Edge | No origin roundtrip, sub-ms verification |
| Cache Pattern | Cache-Aside | Simple, effective, CDN-compatible |
Capability Details
cloudflare-workers
Keywords: cloudflare, workers, kv, durable objects, r2, wrangler Reference: references/cloudflare-workers.md Solves:
- How do I deploy to Cloudflare Workers?
- Cloudflare KV storage patterns
- Durable Objects for stateful edge
- Wrangler CLI usage and configuration
vercel-edge
Keywords: vercel edge, edge functions, edge middleware, geolocation, next.js Reference: references/vercel-edge.md Solves:
- How do I use Vercel Edge Functions?
- Edge middleware patterns (auth, A/B testing)
- Geo-based routing and localization
- Edge streaming responses
runtime-differences
Keywords: edge runtime, web apis, node.js compatibility, polyfills Reference: references/runtime-differences.md Solves:
- What Node.js APIs are NOT available at edge?
- Edge-compatible alternatives to Node APIs
- How to polyfill crypto, base64, buffers
- Package compatibility for edge runtimes
edge-caching
Keywords: edge cache, cdn, cache-control, stale-while-revalidate, invalidation Solves:
- How do I cache at the edge?
- CDN caching strategies and headers
- Stale-while-revalidate patterns
- Cache invalidation strategies
- Personalization at edge
edge-function-template
Keywords: edge function, template, boilerplate, production-ready Solves:
- How do I structure an edge function?
- Production-ready edge function template
- Error handling and validation patterns
- CORS, rate limiting, caching setup
edge-middleware-template
Keywords: middleware, next.js, authentication, a/b testing Solves:
- How do I write Next.js edge middleware?
- Authentication middleware patterns
- A/B testing and feature flags
- Geolocation routing middleware
deployment-checklist
Keywords: deployment, checklist, production, monitoring Reference: checklists/edge-deployment-checklist.md Solves:
- What should I check before deploying to edge?
- Edge deployment best practices
- Production readiness checklist
- Monitoring and debugging setup
Quick Example
// Cloudflare Worker - Basic fetch handler
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
// Geo-based routing
const country = request.cf?.country || 'US';
// Edge caching
const cacheKey = url.pathname + "-" + country;
const cached = await caches.default.match(cacheKey);
if (cached) return cached;
const response = await fetch(request);
return response;
}
}Edge Deployment Checklist
Pre-Deployment Checks
Code Quality
- [ ] Bundle size is optimized (< 1MB compressed for Workers, < 500KB for Edge Functions)
- Run:
npx wrangler deploy --dry-run --outdir=./dist(Cloudflare) - Run:
npm run buildand check.next/staticsize (Vercel) - Use:
npm run analyzeto identify large dependencies
- [ ] No Node.js-specific APIs used
- No
fs,path,child_process,crypto(Node version) - Only Web APIs:
fetch,Request,Response,Headers,crypto.subtle - Check: Run through edge runtime validator
- [ ] All dependencies are edge-compatible
- Verify each package works in V8 isolates
- Test with local edge runtime (
wrangler devornpm run dev) - Replace incompatible packages (see runtime-differences.md)
- [ ] Environment variables are properly configured
- Secrets stored securely (not in code)
- Environment-specific configs separated (dev, staging, prod)
- Cloudflare: Set via
wrangler secret put - Vercel: Set in dashboard or
.env.production
- [ ] Error handling is comprehensive
- All async operations wrapped in try-catch
- Graceful fallbacks for external API failures
- Proper HTTP status codes returned
- Error logging configured (but not blocking)
Performance
- [ ] Cold start time is acceptable (< 50ms target)
- Minimize top-level imports
- Use dynamic imports for heavy dependencies
- Profile with:
console.time('init')in development
- [ ] Response time is optimized (< 100ms target)
- Cache expensive computations
- Use streaming for large responses
- Implement stale-while-revalidate where appropriate
- [ ] Rate limiting is implemented
- Protect against DDoS and abuse
- Use KV/Durable Objects for distributed rate limiting
- Return proper 429 status with Retry-After header
- [ ] Caching strategy is defined
- Cache-Control headers set appropriately
- Edge cache vs browser cache distinction clear
- Cache invalidation strategy documented
Security
- [ ] Authentication is edge-optimized
- JWT verification uses Web Crypto API
- Tokens validated without database calls
- Refresh token mechanism in place
- [ ] CORS is properly configured
- Allowed origins explicitly listed (not
*) - Preflight requests handled correctly
- Credentials mode configured appropriately
- [ ] Security headers are set
X-Frame-Options: DENYX-Content-Type-Options: nosniffContent-Security-PolicyconfiguredStrict-Transport-Securityfor HTTPS
- [ ] Input validation is thorough
- Request body size limits enforced
- Content-Type validation for POST/PUT
- URL parameters sanitized
- No code injection vulnerabilities
- [ ] Secrets are never logged
- No API keys in console.log
- Error messages don't leak sensitive data
- Request/response logging sanitized
Environment Setup
Cloudflare Workers
- [ ] wrangler.toml is configured
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2025-01-01"
[vars]
ENVIRONMENT = "production"- [ ] KV namespaces are created
npx wrangler kv:namespace create MY_KV
npx wrangler kv:namespace create MY_KV --preview- [ ] Durable Objects are registered (if used)
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"- [ ] Routes are configured
routes = [
{ pattern = "example.com/*", zone_name = "example.com" }
]- [ ] Custom domains are set up
- DNS records point to Cloudflare
- SSL/TLS certificates active
- Route patterns match expected traffic
Vercel Edge
- [ ] Project is linked
npx vercel link- [ ] Environment variables are set
npx vercel env add API_KEY production- [ ] Edge Config is created (if using feature flags)
npx vercel edge-config create my-config- [ ] Middleware matcher is correct
export const config = {
matcher: ['/api/:path*', '/dashboard/:path*']
}- [ ] Runtime is explicitly set
export const runtime = 'edge'Deno Deploy
- [ ] GitHub repository is connected
- Deployment triggers configured (main branch)
- Build command specified
- Entry point set correctly
- [ ] Environment variables are added
- In Deno Deploy dashboard
- Separate configs for preview/production
- [ ] Regions are selected
- Primary region closest to users
- Failover regions configured
Testing
Local Testing
- [ ] Local development server runs without errors
- Cloudflare:
npx wrangler dev - Vercel:
npm run dev - Deno:
deno run --allow-net --allow-env main.ts
- [ ] All routes return expected responses
- Test with curl:
curl -X POST http://localhost:8787/api/test - Test in browser:
http://localhost:3000/api/test - Check response headers with dev tools
- [ ] Error cases are handled gracefully
- Test 404, 401, 403, 429, 500 responses
- Verify error messages are user-friendly
- Check logs for proper error tracking
- [ ] Performance is acceptable locally
- Response times < 100ms for simple requests
- No memory leaks (test with sustained load)
- CPU usage reasonable (< 30% spike)
Integration Testing
- [ ] External API integrations work
- Test with real API keys (staging environment)
- Handle timeout scenarios
- Verify retry logic
- [ ] Database connections succeed (if applicable)
- Connection pooling configured
- Query timeouts set appropriately
- Fallback to cache on DB failure
- [ ] Authentication flow is tested
- Valid tokens accepted
- Invalid tokens rejected
- Expired tokens refresh properly
- [ ] Caching behavior is verified
- Cache hits return stale data appropriately
- Cache misses fetch fresh data
- Cache invalidation works
Load Testing
- [ ] Concurrent request handling
- Test with tools:
ab,wrk,autocannon - Example:
ab -n 1000 -c 10 https://example.com/api/test - Verify no dropped requests
- [ ] Rate limiting is effective
- Exceed rate limit and verify 429 response
- Wait for window reset and verify access restored
- [ ] Edge locations respond consistently
- Test from multiple geographic regions
- Use:
curl --resolveor multi-region testing tools - Verify response times < 100ms globally
Deployment
Pre-Deployment
- [ ] Code is reviewed and approved
- Peer review completed
- Security review for sensitive changes
- No console.log statements in production code
- [ ] Tests pass in CI/CD
- Unit tests: 100% pass rate
- Integration tests: All critical paths covered
- No linting errors
- [ ] Changelog is updated
- Document breaking changes
- List new features
- Note deprecations
Deployment Process
- [ ] Deploy to staging first
- Cloudflare:
npx wrangler deploy --env staging - Vercel:
npx vercel --target preview - Test staging thoroughly before production
- [ ] Smoke test staging deployment
- Hit all critical endpoints
- Verify authentication works
- Check logs for errors
- [ ] Deploy to production
- Cloudflare:
npx wrangler deploy - Vercel:
npx vercel --prod - Deno: Push to main branch (auto-deploys)
- [ ] Verify deployment success
- Check deployment logs for errors
- Verify version number updated
- Test production URL responds
Post-Deployment
- [ ] Monitor initial traffic
- Watch error rates (should be < 0.1%)
- Check response times (should be < 100ms p95)
- Verify cache hit ratio (> 70% for cacheable content)
- [ ] Test critical user flows
- Login/logout
- API requests
- Page loads
- [ ] Check logs for unexpected errors
- Cloudflare:
npx wrangler tail - Vercel: Check dashboard logs
- Set up alerts for error spikes
- [ ] Validate edge distribution
- Requests hitting multiple edge locations
- No single region overwhelmed
- Latency consistent globally
Monitoring and Debugging
Observability Setup
- [ ] Logging is configured
- Structured logs (JSON format)
- Log levels appropriate (ERROR, WARN, INFO)
- No sensitive data in logs
- [ ] Metrics are tracked
- Request count
- Error rate
- Response time (p50, p95, p99)
- Cache hit ratio
- [ ] Alerting is set up
- Error rate > 1% triggers alert
- Response time p95 > 500ms triggers alert
- Rate limit violations tracked
- [ ] Dashboard is configured
- Cloudflare: Analytics tab
- Vercel: Analytics dashboard
- Custom: Grafana, Datadog, etc.
Debugging Tools
- [ ] Live logs are accessible
- Cloudflare:
npx wrangler tail - Vercel: Dashboard → Functions → Logs
- Real-time filtering works
- [ ] Edge locations are identified
- Add
X-Edge-Locationheader in responses - Track which regions serve traffic
- Identify regional issues
- [ ] Request tracing is enabled
- Unique request IDs in responses
- Correlation IDs for multi-service requests
- Trace logs across services
Rollback Plan
- [ ] Previous version is identified
- Cloudflare:
npx wrangler deployments list - Vercel: Dashboard → Deployments
- Git commit hash recorded
- [ ] Rollback command is documented
- Cloudflare:
npx wrangler rollback --message "Revert bad deploy" - Vercel: Dashboard → Deployments → Promote previous
- Tested in staging environment
- [ ] Rollback criteria are defined
- Error rate > 5%
- Response time p95 > 1s
- Critical feature broken
Compliance and Documentation
- [ ] Privacy policy updated (if collecting user data)
- [ ] Terms of service reflect edge processing
- [ ] GDPR compliance verified (for EU users)
- [ ] API documentation updated
- [ ] Runbook created (deployment, rollback, debugging)
- [ ] Team trained on edge deployment process
Platform-Specific Checks
Cloudflare Workers Only
- [ ] Subrequest limits considered (50 on free, 1000 on paid)
- [ ] CPU time within limits (10ms free, 30s paid)
- [ ] KV eventually consistent behavior handled (60s propagation)
- [ ] Durable Objects isolated per-object (not global state)
Vercel Edge Only
- [ ] Function size within limits (1MB compressed)
- [ ] Execution time within limits (25s Hobby, 30s Pro)
- [ ] Edge Config read-only (writes go through API)
- [ ] Middleware doesn't block rendering (fast execution)
Deno Deploy Only
- [ ] Import specifiers are full URLs (not bare imports)
- [ ] Dependencies are pinned (versioned CDN URLs)
- [ ] Standard library used (deno.land/std@0.x.x)
- [ ] Permissions are minimal (only necessary --allow-* flags)
Edge Caching Strategies
Overview
Edge caching reduces latency and backend load by serving responses from the nearest edge location. This guide covers Cache-Control headers, cache invalidation, and personalization strategies for 2025.
Cache-Control Headers
Basic Patterns
Public Static Assets (Immutable)
// Images, fonts, versioned JS/CSS
export async function GET(request: Request) {
const response = await fetch('https://cdn.example.com/app.v123.js')
return new Response(response.body, {
headers: {
'Content-Type': 'application/javascript',
'Cache-Control': 'public, max-age=31536000, immutable'
// 1 year cache, never revalidate (version in filename)
}
})
}Dynamic Content (Short Cache)
// User dashboards, personalized feeds
export async function GET(request: Request) {
const data = await fetchDynamicContent()
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'private, max-age=60, must-revalidate'
// 1 minute cache, only in browser, revalidate when stale
}
})
}Semi-Static Content (Medium Cache)
// Blog posts, product pages
export async function GET(request: Request) {
const post = await fetchBlogPost()
return new Response(JSON.stringify(post), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600, s-maxage=86400, stale-while-revalidate=86400'
// Browser: 1 hour, CDN: 24 hours, serve stale for 24 hours while revalidating
}
})
}No Cache (Always Fresh)
// Real-time data, stock prices
export async function GET(request: Request) {
const liveData = await fetchLiveData()
return new Response(JSON.stringify(liveData), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, no-cache, must-revalidate'
// Never cache, always fetch fresh
}
})
}Stale-While-Revalidate
The stale-while-revalidate directive serves stale content while fetching fresh data in the background.
Basic Implementation
export async function GET(request: Request) {
const url = new URL(request.url)
const cacheKey = new Request(url.toString())
const cache = caches.default
// Try cache first
let response = await cache.match(cacheKey)
if (response) {
// Serve from cache
const age = parseInt(response.headers.get('Age') || '0')
const maxAge = 300 // 5 minutes
// If stale, revalidate in background
if (age > maxAge) {
// Don't await - serve stale immediately
revalidateInBackground(cacheKey, cache)
}
return response
}
// Cache miss - fetch fresh
response = await fetchFresh(request)
await cache.put(cacheKey, response.clone())
return response
}
async function revalidateInBackground(cacheKey: Request, cache: Cache) {
try {
const fresh = await fetchFresh(cacheKey)
await cache.put(cacheKey, fresh)
} catch (error) {
console.error('Background revalidation failed:', error)
}
}
async function fetchFresh(request: Request): Promise<Response> {
const data = await fetch('https://api.example.com/data')
return new Response(data.body, {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=300, stale-while-revalidate=3600',
'Age': '0'
}
})
}Advanced: Stale-If-Error
export async function GET(request: Request) {
const cache = caches.default
const cacheKey = new Request(request.url)
try {
// Try fetching fresh data
const response = await fetchWithTimeout('https://api.example.com/data', 5000)
// Cache successful response
await cache.put(cacheKey, response.clone())
return response
} catch (error) {
// Origin failed - serve stale if available
const stale = await cache.match(cacheKey)
if (stale) {
console.warn('Serving stale due to error:', error)
return new Response(stale.body, {
...stale,
headers: {
...Object.fromEntries(stale.headers),
'X-Served-Stale': 'true',
'X-Stale-Reason': 'origin-error'
}
})
}
// No stale version - return error
throw error
}
}
async function fetchWithTimeout(url: string, timeout: number): Promise<Response> {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, { signal: controller.signal })
clearTimeout(timer)
return response
} catch (error) {
clearTimeout(timer)
throw error
}
}Cache Invalidation
Tag-Based Invalidation (Cloudflare)
// worker.ts
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
// Invalidate cache by tag
if (url.pathname === '/api/purge' && request.method === 'POST') {
const { tags } = await request.json()
// Cloudflare Cache API with tags
await fetch('https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.CF_API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ tags })
})
return new Response('Cache purged', { status: 200 })
}
// Serve with cache tags
const response = await fetch('https://origin.example.com' + url.pathname)
return new Response(response.body, {
headers: {
...Object.fromEntries(response.headers),
'Cache-Tag': 'blog,post-123' // Tag for invalidation
}
})
}
}Manual Cache Deletion
export async function DELETE(request: Request) {
const url = new URL(request.url)
const cacheKey = url.searchParams.get('key')
if (!cacheKey) {
return new Response('Missing key', { status: 400 })
}
const cache = caches.default
const deleted = await cache.delete(new Request(`https://cache/${cacheKey}`))
return new Response(
JSON.stringify({ deleted }),
{ headers: { 'Content-Type': 'application/json' } }
)
}Time-Based Invalidation (TTL)
// Cloudflare KV with TTL
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const key = 'data:latest'
// Try KV cache
const cached = await env.CACHE_KV.get(key, 'json')
if (cached) {
return new Response(JSON.stringify(cached), {
headers: {
'Content-Type': 'application/json',
'X-Cache': 'HIT'
}
})
}
// Fetch fresh data
const fresh = await fetchData()
// Store with 5-minute TTL
await env.CACHE_KV.put(key, JSON.stringify(fresh), {
expirationTtl: 300
})
return new Response(JSON.stringify(fresh), {
headers: {
'Content-Type': 'application/json',
'X-Cache': 'MISS'
}
})
}
}Personalization at Edge
Cookie-Based Personalization
export async function GET(request: Request) {
const userId = request.headers.get('Cookie')?.match(/userId=([^;]+)/)?.[1]
if (userId) {
// Serve personalized content (don't cache in CDN)
const personalData = await fetchPersonalData(userId)
return new Response(JSON.stringify(personalData), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'private, max-age=60'
// Only cache in browser, not CDN
}
})
}
// Serve generic content (cache in CDN)
const genericData = await fetchGenericData()
return new Response(JSON.stringify(genericData), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600'
}
})
}Vary Header for Multiple Versions
export async function GET(request: Request) {
const country = request.headers.get('CF-IPCountry') || 'US'
const language = request.headers.get('Accept-Language')?.split(',')[0] || 'en'
// Fetch localized content
const content = await fetchLocalizedContent(country, language)
return new Response(JSON.stringify(content), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600',
'Vary': 'CF-IPCountry, Accept-Language'
// Cache separate versions for each country/language
}
})
}Edge-Side Includes (ESI) Pattern
// Compose cached fragments at edge
export async function GET(request: Request) {
const cache = caches.default
// Fetch cached fragments
const [header, personalContent, footer] = await Promise.all([
cache.match(new Request('https://cache/header')),
fetchPersonalContent(), // Always fresh
cache.match(new Request('https://cache/footer'))
])
const html = `
${await header?.text() || ''}
${personalContent}
${await footer?.text() || ''}
`
return new Response(html, {
headers: {
'Content-Type': 'text/html',
'Cache-Control': 'private, max-age=0'
// Don't cache composed page
}
})
}Advanced Caching Patterns
Conditional Requests (ETag)
export async function GET(request: Request) {
const data = await fetchData()
const etag = await generateETag(data)
// Check If-None-Match header
const clientETag = request.headers.get('If-None-Match')
if (clientETag === etag) {
return new Response(null, {
status: 304, // Not Modified
headers: { 'ETag': etag }
})
}
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'ETag': etag,
'Cache-Control': 'public, max-age=60'
}
})
}
async function generateETag(data: any): Promise<string> {
const hash = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(JSON.stringify(data))
)
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
.slice(0, 16)
}Multi-Tier Caching
export async function GET(request: Request, env: Env) {
const key = 'data:product-123'
// Tier 1: Edge cache (fastest)
const edgeCache = caches.default
let response = await edgeCache.match(request)
if (response) {
return addCacheHeader(response, 'edge')
}
// Tier 2: KV cache (fast, distributed)
const kvData = await env.CACHE_KV.get(key, 'json')
if (kvData) {
response = new Response(JSON.stringify(kvData), {
headers: { 'Content-Type': 'application/json' }
})
await edgeCache.put(request, response.clone())
return addCacheHeader(response, 'kv')
}
// Tier 3: Database (slow)
const dbData = await fetchFromDatabase()
response = new Response(JSON.stringify(dbData), {
headers: { 'Content-Type': 'application/json' }
})
// Populate caches
await env.CACHE_KV.put(key, JSON.stringify(dbData), { expirationTtl: 3600 })
await edgeCache.put(request, response.clone())
return addCacheHeader(response, 'database')
}
function addCacheHeader(response: Response, tier: string): Response {
const newResponse = new Response(response.body, response)
newResponse.headers.set('X-Cache-Tier', tier)
return newResponse
}Performance Monitoring
Cache Hit Ratio Tracking
let cacheHits = 0
let cacheMisses = 0
export async function GET(request: Request) {
const cache = caches.default
const cached = await cache.match(request)
if (cached) {
cacheHits++
console.log(`Cache hit ratio: ${(cacheHits / (cacheHits + cacheMisses) * 100).toFixed(2)}%`)
return cached
}
cacheMisses++
const fresh = await fetchFresh(request)
await cache.put(request, fresh.clone())
return fresh
}Cache Analytics Headers
export async function GET(request: Request) {
const startTime = Date.now()
const cache = caches.default
const cached = await cache.match(request)
const cacheTime = Date.now() - startTime
if (cached) {
const age = parseInt(cached.headers.get('Age') || '0')
const response = new Response(cached.body, cached)
response.headers.set('X-Cache', 'HIT')
response.headers.set('X-Cache-Age', age.toString())
response.headers.set('X-Cache-Lookup-Time', `${cacheTime}ms`)
return response
}
const fetchStart = Date.now()
const fresh = await fetchFresh(request)
const fetchTime = Date.now() - fetchStart
fresh.headers.set('X-Cache', 'MISS')
fresh.headers.set('X-Origin-Time', `${fetchTime}ms`)
return fresh
}Cloudflare Workers Reference
Overview
Cloudflare Workers run on Cloudflare's global network of 300+ edge locations using V8 isolates (not containers), providing sub-millisecond cold starts and unlimited concurrency.
Runtime Constraints
Execution Limits
- CPU Time: 10ms (free), 30s (paid), unlimited (Enterprise)
- Memory: 128 MB per request
- Subrequests: 50 outbound fetch() calls per request (free), 1000 (paid)
- Script Size: 1 MB compressed, 10 MB uncompressed
- Environment Variables: 64 KB total size
Duration Patterns
// Good: Fast edge computation (<10ms)
export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url)
const cached = await caches.default.match(request)
if (cached) return cached
return new Response('Fast response')
}
}
// Risky: Heavy computation (may timeout on free tier)
export default {
async fetch(request: Request): Promise<Response> {
// Expensive image processing, large JSON parsing
const data = await fetch('https://api.example.com/large-dataset')
const json = await data.json() // Could be 100MB+
// Process json... (CPU-intensive)
return new Response(JSON.stringify(result))
}
}KV Storage Patterns
Cloudflare KV is an eventually consistent key-value store optimized for high-read, low-write scenarios.
Basic Usage
interface Env {
MY_KV: KVNamespace
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Write (takes 60s to propagate globally)
await env.MY_KV.put('user:123', JSON.stringify({ name: 'Alice' }), {
expirationTtl: 3600, // 1 hour
metadata: { createdAt: Date.now() }
})
// Read (fast, from nearest edge)
const value = await env.MY_KV.get('user:123', 'json')
// List keys (expensive, paginate for many keys)
const keys = await env.MY_KV.list({ prefix: 'user:', limit: 100 })
return new Response(JSON.stringify(value))
}
}KV Best Practices
// ✅ Good: Cache with TTL
await env.CACHE.put('api:response', data, { expirationTtl: 300 })
// ✅ Good: Namespace keys to avoid conflicts
await env.KV.put(`tenant:${tenantId}:config`, config)
// ❌ Bad: Frequent writes (eventual consistency causes issues)
for (let i = 0; i < 100; i++) {
await env.KV.put(`counter:${i}`, i.toString()) // Takes 60s each!
}
// ✅ Better: Use Durable Objects for frequent writes
const id = env.COUNTER.idFromName('global')
const stub = env.COUNTER.get(id)
await stub.fetch(new Request('https://counter/increment'))Durable Objects for Stateful Edge
Durable Objects provide strong consistency and persistent state with automatic migration across edge locations.
Counter Example
// durable-objects/counter.ts
export class Counter {
private state: DurableObjectState
private count: number = 0
private initialized = false
constructor(state: DurableObjectState, env: Env) {
this.state = state
}
async fetch(request: Request): Promise<Response> {
// Lazy initialization
if (!this.initialized) {
this.count = (await this.state.storage.get<number>('count')) || 0
this.initialized = true
}
const url = new URL(request.url)
if (url.pathname === '/increment') {
this.count++
await this.state.storage.put('count', this.count)
}
if (url.pathname === '/decrement') {
this.count--
await this.state.storage.put('count', this.count)
}
return new Response(JSON.stringify({ count: this.count }), {
headers: { 'Content-Type': 'application/json' }
})
}
}WebSocket Chat Room
export class ChatRoom {
private state: DurableObjectState
private sessions: Set<WebSocket> = new Set()
constructor(state: DurableObjectState, env: Env) {
this.state = state
}
async fetch(request: Request): Promise<Response> {
const upgradeHeader = request.headers.get('Upgrade')
if (upgradeHeader !== 'websocket') {
return new Response('Expected WebSocket', { status: 426 })
}
const pair = new WebSocketPair()
const [client, server] = Object.values(pair)
this.sessions.add(server)
server.addEventListener('message', (event) => {
// Broadcast to all connected clients
this.sessions.forEach((session) => {
if (session !== server) {
session.send(event.data)
}
})
})
server.addEventListener('close', () => {
this.sessions.delete(server)
})
server.accept()
return new Response(null, { status: 101, webSocket: client })
}
}Wrangler CLI Usage
Project Initialization
# Create new Worker project
npm create cloudflare@latest my-worker -- --type=worker
# Or with TypeScript template
npm create cloudflare@latest my-worker -- --type=worker --ts
# Install dependencies
cd my-worker
npm installDevelopment Workflow
# Local development (with hot reload)
npx wrangler dev
# Local dev with remote KV/Durable Objects
npx wrangler dev --remote
# Tail live logs from production
npx wrangler tail
# Tail with filters
npx wrangler tail --status error --method POSTDeployment
# Deploy to production
npx wrangler deploy
# Deploy to preview environment
npx wrangler deploy --env staging
# Rollback to previous version
npx wrangler rollback --message "Reverting bad deploy"Managing Secrets
# Set secret (interactive prompt)
npx wrangler secret put API_KEY
# Bulk secrets from .env file
npx wrangler secret bulk .env.production
# Delete secret
npx wrangler secret delete API_KEYKV and Durable Objects
# Create KV namespace
npx wrangler kv:namespace create MY_KV
# Put key-value
npx wrangler kv:key put --namespace-id=<id> "myKey" "myValue"
# Get value
npx wrangler kv:key get --namespace-id=<id> "myKey"
# List Durable Object instances
npx wrangler durable-objects list COUNTERwrangler.toml Configuration
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2025-01-01"
# KV Bindings
kv_namespaces = [
{ binding = "MY_KV", id = "abc123", preview_id = "xyz789" }
]
# Durable Objects
durable_objects.bindings = [
{ name = "COUNTER", class_name = "Counter" },
{ name = "CHAT_ROOM", class_name = "ChatRoom" }
]
[[migrations]]
tag = "v1"
new_classes = ["Counter", "ChatRoom"]
# R2 Bindings (object storage)
r2_buckets = [
{ binding = "MY_BUCKET", bucket_name = "my-bucket" }
]
# Environment Variables
[vars]
ENVIRONMENT = "production"
# Staging environment override
[env.staging]
name = "my-worker-staging"
kv_namespaces = [
{ binding = "MY_KV", id = "staging-id" }
]Performance Tips
Cold Start Optimization
// ✅ Good: Minimal imports, lazy initialization
export default {
async fetch(request: Request): Promise<Response> {
const result = await handleRequest(request)
return new Response(result)
}
}
// ❌ Bad: Heavy imports at top-level
import heavyLibrary from 'heavy-library' // Adds 500ms cold startCaching Strategies
// Cache expensive computations
const cache = caches.default
async function getExpensiveData(key: string): Promise<any> {
const cacheKey = new Request(`https://cache/${key}`)
let response = await cache.match(cacheKey)
if (!response) {
const data = await computeExpensiveData(key)
response = new Response(JSON.stringify(data), {
headers: {
'Cache-Control': 'max-age=3600',
'Content-Type': 'application/json'
}
})
await cache.put(cacheKey, response.clone())
}
return response.json()
}Common Gotchas
1. KV is eventually consistent: Writes take ~60s to propagate globally 2. No persistent state in Workers: Use Durable Objects for stateful logic 3. Subrequest limits: 50 fetch() calls on free tier 4. No Node.js APIs: Must use Web APIs only 5. CPU time includes async waits: Use streaming to avoid timeouts
Edge Runtime vs Node.js Runtime Differences
Overview
Edge runtimes use the Web Standard APIs (V8 isolates) instead of Node.js, providing faster cold starts but with limited functionality. This guide helps you write edge-compatible code.
Runtime API Comparison
Available in Edge Runtime
Fetch & Networking
// ✅ Available: Web Fetch API
const response = await fetch('https://api.example.com')
const data = await response.json()
// ✅ Available: Request/Response
const request = new Request('https://example.com', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'value' })
})
// ✅ Available: Headers
const headers = new Headers()
headers.set('X-Custom', 'value')
// ✅ Available: URL & URLSearchParams
const url = new URL('https://example.com/path?query=value')
const params = new URLSearchParams(url.search)Text Processing
// ✅ Available: TextEncoder/TextDecoder
const encoder = new TextEncoder()
const bytes = encoder.encode('Hello')
const decoder = new TextDecoder()
const text = decoder.decode(bytes)
// ✅ Available: atob/btoa (base64)
const encoded = btoa('Hello World')
const decoded = atob(encoded)Cryptography
// ✅ Available: Web Crypto API
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
)
const data = new TextEncoder().encode('secret')
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(12)) },
key,
data
)
// ✅ Available: crypto.randomUUID()
const id = crypto.randomUUID()Streams
// ✅ Available: ReadableStream, WritableStream, TransformStream
const stream = new ReadableStream({
start(controller) {
controller.enqueue('chunk 1')
controller.enqueue('chunk 2')
controller.close()
}
})
const transform = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase())
}
})
const transformed = stream.pipeThrough(transform)Timers
// ✅ Available: setTimeout, setInterval, clearTimeout, clearInterval
const timer = setTimeout(() => console.log('Delayed'), 1000)
clearTimeout(timer)
// ✅ Available: Promise APIs
await Promise.all([fetch('/a'), fetch('/b')])
await Promise.race([fetch('/a'), fetch('/b')])NOT Available in Edge Runtime
File System
// ❌ NOT Available: fs module
import fs from 'fs' // Error!
fs.readFileSync('./file.txt') // Error!
// ✅ Alternative: Fetch from origin or use R2/S3
const file = await fetch('https://cdn.example.com/file.txt')
const content = await file.text()Path & OS
// ❌ NOT Available: path module
import path from 'path' // Error!
path.join('/foo', 'bar') // Error!
// ✅ Alternative: Manual string manipulation or URL
const joined = '/foo/bar'
const url = new URL('/foo/bar', 'https://example.com')Process & Environment
// ❌ NOT Available: process.cwd(), process.env (limited)
process.cwd() // Error!
// ✅ Available: env vars via platform-specific binding
// Cloudflare Workers
export default {
async fetch(request: Request, env: Env) {
const apiKey = env.API_KEY // From wrangler.toml
}
}
// Vercel Edge
export const runtime = 'edge'
export async function GET() {
const apiKey = process.env.API_KEY // From Vercel env vars
}Child Processes
// ❌ NOT Available: child_process
import { exec } from 'child_process' // Error!
exec('ls -la') // Error!
// ✅ Alternative: Call external API for heavy compute
const result = await fetch('https://compute-api.example.com/process')Node.js Crypto
// ❌ NOT Available: Node.js crypto module
import crypto from 'crypto' // Error!
crypto.createHash('sha256') // Error!
// ✅ Alternative: Web Crypto API
const data = new TextEncoder().encode('data')
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')Buffers
// ❌ NOT Available: Node.js Buffer
const buf = Buffer.from('hello') // Error!
// ✅ Alternative: Uint8Array
const arr = new TextEncoder().encode('hello')
const decoded = new TextDecoder().decode(arr)Polyfills and Alternatives
Base64 Encoding/Decoding
// Node.js way (NOT available)
// const encoded = Buffer.from('hello').toString('base64')
// Edge-compatible way
function base64Encode(str: string): string {
return btoa(str)
}
function base64Decode(str: string): string {
return atob(str)
}
// For binary data
function base64EncodeBytes(bytes: Uint8Array): string {
const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join('')
return btoa(binString)
}
function base64DecodeBytes(str: string): Uint8Array {
const binString = atob(str)
return Uint8Array.from(binString, (m) => m.codePointAt(0)!)
}SHA-256 Hashing
// Node.js way (NOT available)
// const hash = crypto.createHash('sha256').update('data').digest('hex')
// Edge-compatible way
async function sha256(message: string): Promise<string> {
const msgBuffer = new TextEncoder().encode(message)
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
// Usage
const hash = await sha256('hello world')HMAC Signing
// Node.js way (NOT available)
// const hmac = crypto.createHmac('sha256', secret).update(data).digest('hex')
// Edge-compatible way
async function hmacSign(secret: string, data: string): Promise<string> {
const encoder = new TextEncoder()
const keyData = encoder.encode(secret)
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
)
const signature = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(data)
)
const hashArray = Array.from(new Uint8Array(signature))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}
// Usage
const signature = await hmacSign('secret-key', 'message')JWT Verification
// Node.js way (jsonwebtoken package - NOT compatible)
// import jwt from 'jsonwebtoken'
// Edge-compatible way (use @tsndr/cloudflare-worker-jwt)
import { verify } from '@tsndr/cloudflare-worker-jwt'
async function verifyToken(token: string, secret: string): Promise<boolean> {
try {
const isValid = await verify(token, secret)
return isValid
} catch (error) {
return false
}
}Random Number Generation
// Node.js way (NOT available)
// const random = crypto.randomBytes(16)
// Edge-compatible way
function getRandomBytes(length: number): Uint8Array {
return crypto.getRandomValues(new Uint8Array(length))
}
function getRandomInt(min: number, max: number): number {
const range = max - min
const bytes = crypto.getRandomValues(new Uint32Array(1))
return min + (bytes[0] % range)
}
// UUID generation (built-in!)
const uuid = crypto.randomUUID()Cold Start Optimization
Import Strategies
// ❌ Bad: Large top-level imports (increases cold start)
import { heavy, unused, functions } from 'large-library'
export default {
async fetch() {
return new Response(heavy())
}
}
// ✅ Good: Dynamic imports (faster cold start)
export default {
async fetch() {
const { heavy } = await import('large-library')
return new Response(heavy())
}
}
// ✅ Better: Import only what's needed
import { specificFunction } from 'large-library/specific'Lazy Initialization
// ❌ Bad: Initialize at top-level
const expensiveComputation = computeExpensiveValue() // Runs on every cold start!
export default {
async fetch() {
return new Response(expensiveComputation)
}
}
// ✅ Good: Lazy initialization
let cachedValue: string | null = null
export default {
async fetch() {
if (!cachedValue) {
cachedValue = computeExpensiveValue()
}
return new Response(cachedValue)
}
}Bundle Size Optimization
// ❌ Bad: Import entire library
import _ from 'lodash' // 70KB!
// ✅ Good: Import specific function
import debounce from 'lodash.debounce' // 2KB
// ✅ Better: Use native alternatives
const unique = [...new Set(array)]
const mapped = array.map(x => x * 2)Testing Edge-Compatible Code
Local Testing
# Cloudflare Workers
npx wrangler dev
# Vercel Edge (Next.js)
npm run dev
# Deno Deploy
deno run --allow-net --allow-env main.tsUnit Testing with Miniflare (Cloudflare)
import { Miniflare } from 'miniflare'
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
describe('Worker', () => {
let mf: Miniflare
beforeAll(() => {
mf = new Miniflare({
script: `
export default {
async fetch(request) {
return new Response('Hello!')
}
}
`
})
})
afterAll(() => mf.dispose())
it('returns response', async () => {
const response = await mf.dispatchFetch('https://example.com')
expect(await response.text()).toBe('Hello!')
})
})Package Compatibility
Edge-Compatible Packages
- ✅
@tsndr/cloudflare-worker-jwt- JWT for edge - ✅
zod- Schema validation - ✅
date-fns- Date utilities (tree-shakeable) - ✅
nanoid- ID generation - ✅
hono- Lightweight router - ✅
@vercel/edge-config- Feature flags
NOT Edge-Compatible (Common Mistakes)
- ❌
jsonwebtoken- Uses Node.js crypto - ❌
bcrypt- Native bindings - ❌
sharp- Image processing (use Cloudflare Images API) - ❌
prisma- Database ORM (use@prisma/client/edgewith Data Proxy) - ❌
axios- Use nativefetchinstead - ❌
moment- Large bundle, usedate-fnsor nativeIntl
Common Gotchas
1. No `__dirname` or `__filename`: Use import.meta.url instead (Deno/modern runtimes) 2. Limited `process.env`: Use platform-specific env bindings 3. No synchronous file I/O: Everything must be async 4. No `Buffer`: Use Uint8Array and TextEncoder/TextDecoder 5. Strict Content Security Policy: Some eval-based libraries won't work
Vercel Edge Functions Reference
Overview
Vercel Edge Functions run on Vercel's global network using V8 isolates, providing low-latency responses across 100+ regions. They integrate seamlessly with Next.js for middleware and API routes.
Edge Functions vs Serverless Functions
| Feature | Edge Functions | Serverless Functions |
|---|---|---|
| Runtime | V8 Isolates | Node.js |
| Cold Start | <10ms | 50-200ms |
| Max Duration | 25s (Hobby), 30s (Pro) | 10s (Hobby), 60s (Pro) |
| Memory | 128 MB | 1024 MB |
| APIs | Web APIs only | Full Node.js |
| Best For | Auth, routing, headers | Database queries, heavy compute |
When to Use Edge Functions
// ✅ Good: Authentication check (fast, no DB)
export const runtime = 'edge'
export async function GET(request: Request) {
const token = request.headers.get('Authorization')
const isValid = await verifyJWT(token)
if (!isValid) {
return new Response('Unauthorized', { status: 401 })
}
return new Response('Authorized')
}
// ❌ Bad: Heavy database queries (use serverless instead)
export const runtime = 'edge' // Wrong choice!
export async function GET() {
// Edge has no connection pooling, no Prisma, limited DB drivers
const users = await db.query('SELECT * FROM users') // Will be slow/fail
return new Response(JSON.stringify(users))
}Edge Middleware Patterns
Middleware runs before every request, enabling global request/response modification.
Basic Middleware
// middleware.ts (root of project)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// Add custom header
const response = NextResponse.next()
response.headers.set('X-Custom-Header', 'value')
return response
}
// Run on all routes
export const config = {
matcher: '/:path*'
}A/B Testing Middleware
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const url = request.nextUrl.clone()
// Check existing bucket cookie
let bucket = request.cookies.get('ab-test-bucket')?.value
if (!bucket) {
// Assign new bucket (50/50 split)
bucket = Math.random() < 0.5 ? 'control' : 'variant'
}
// Rewrite to appropriate page
if (bucket === 'variant') {
url.pathname = `/variant${url.pathname}`
}
const response = NextResponse.rewrite(url)
// Set persistent cookie
response.cookies.set('ab-test-bucket', bucket, {
maxAge: 60 * 60 * 24 * 30, // 30 days
httpOnly: true,
sameSite: 'strict'
})
return response
}
export const config = {
matcher: '/landing-page'
}Geolocation-Based Routing
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US'
const city = request.geo?.city
const region = request.geo?.region
const url = request.nextUrl.clone()
// Redirect EU users to GDPR-compliant page
const euCountries = ['DE', 'FR', 'IT', 'ES', 'NL', 'GB']
if (euCountries.includes(country)) {
url.pathname = '/eu' + url.pathname
return NextResponse.rewrite(url)
}
// Pass geo data to page
const response = NextResponse.next()
response.headers.set('X-User-Country', country)
response.headers.set('X-User-City', city || 'Unknown')
return response
}
export const config = {
matcher: '/:path*'
}Authentication Middleware
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { verifyAuth } from '@/lib/auth'
export async function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')?.value
// Protected routes
if (request.nextUrl.pathname.startsWith('/dashboard')) {
if (!token) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('from', request.nextUrl.pathname)
return NextResponse.redirect(loginUrl)
}
// Verify token at edge (fast!)
const isValid = await verifyAuth(token)
if (!isValid) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/api/private/:path*']
}Streaming Responses
Edge Functions support streaming for real-time data delivery without buffering.
Basic Streaming
export const runtime = 'edge'
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
// Send chunks over time
controller.enqueue(encoder.encode('data: chunk 1\n\n'))
await new Promise(resolve => setTimeout(resolve, 1000))
controller.enqueue(encoder.encode('data: chunk 2\n\n'))
await new Promise(resolve => setTimeout(resolve, 1000))
controller.enqueue(encoder.encode('data: chunk 3\n\n'))
controller.close()
}
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
})
}LLM Streaming Response
export const runtime = 'edge'
export async function POST(request: Request) {
const { prompt } = await request.json()
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: prompt }],
stream: true
})
})
// Stream OpenAI response directly to client
return new Response(response.body, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache'
}
})
}Transform Stream Pattern
export const runtime = 'edge'
export async function GET() {
const upstream = await fetch('https://api.example.com/data')
const transformStream = new TransformStream({
transform(chunk, controller) {
// Modify each chunk (e.g., add metadata)
const modified = JSON.parse(chunk)
modified.timestamp = Date.now()
controller.enqueue(JSON.stringify(modified))
}
})
return new Response(upstream.body?.pipeThrough(transformStream), {
headers: { 'Content-Type': 'application/json' }
})
}Edge Config for Feature Flags
Edge Config is a globally distributed read-only data store optimized for feature flags and configuration.
Setup Edge Config
# Create Edge Config
npx vercel env pull
npx vercel edge-config create my-config
# Link to project
npx vercel linkUsage in Edge Function
import { get } from '@vercel/edge-config'
export const runtime = 'edge'
export async function GET(request: Request) {
// Read feature flag (sub-millisecond latency)
const enableNewFeature = await get('enable_new_feature')
if (enableNewFeature) {
return new Response('New feature enabled!')
}
return new Response('Old feature')
}Advanced Edge Config Patterns
import { get, getAll } from '@vercel/edge-config'
export async function middleware(request: NextRequest) {
// Get all config at once (faster than multiple get() calls)
const config = await getAll([
'maintenance_mode',
'allowed_countries',
'rate_limit'
])
// Maintenance mode
if (config.maintenance_mode === true) {
return new Response('Site under maintenance', { status: 503 })
}
// Country blocking
const country = request.geo?.country
if (!config.allowed_countries.includes(country)) {
return new Response('Not available in your region', { status: 403 })
}
// Dynamic rate limiting
const rateLimit = config.rate_limit || 100
// ... rate limit logic
return NextResponse.next()
}Edge API Routes
Basic Edge API Route
// app/api/hello/route.ts
export const runtime = 'edge'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const name = searchParams.get('name') || 'World'
return new Response(JSON.stringify({ message: `Hello, ${name}!` }), {
headers: { 'Content-Type': 'application/json' }
})
}Edge API with Environment Variables
export const runtime = 'edge'
export async function POST(request: Request) {
const body = await request.json()
// Access env vars (set in Vercel dashboard)
const apiKey = process.env.API_KEY
const region = process.env.VERCEL_REGION // Automatic var
const response = await fetch('https://api.example.com/endpoint', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'X-Region': region
},
body: JSON.stringify(body)
})
return new Response(response.body, {
status: response.status,
headers: { 'Content-Type': 'application/json' }
})
}Performance Tips
Reduce Bundle Size
// ❌ Bad: Large dependency
import { hugeLibrary } from 'huge-library' // 500KB!
// ✅ Good: Lightweight edge-compatible library
import { tinyLibrary } from '@edge/tiny-library' // 5KBCache Expensive Computations
import { unstable_cache } from 'next/cache'
export const runtime = 'edge'
const getCachedData = unstable_cache(
async (userId: string) => {
// Expensive operation
const data = await fetchUserData(userId)
return data
},
['user-data'],
{ revalidate: 300 } // 5 minutes
)
export async function GET(request: Request) {
const userId = new URL(request.url).searchParams.get('userId')
const data = await getCachedData(userId!)
return new Response(JSON.stringify(data))
}Common Gotchas
1. No Node.js APIs: Must use Web APIs only (no fs, path, crypto from Node) 2. 25-30s timeout: Long-running tasks need serverless functions 3. No connection pooling: Database connections are expensive 4. No file uploads: Large request bodies should use serverless 5. Limited npm packages: Many packages depend on Node.js APIs
/**
* Edge Function Template
*
* A production-ready edge function template with error handling,
* validation, caching, and observability.
*
* Compatible with: Cloudflare Workers, Vercel Edge, Deno Deploy
*/
// ============================================================================
// Types & Interfaces
// ============================================================================
interface Env {
// Environment variables (Cloudflare Workers style)
API_KEY?: string
DATABASE_URL?: string
CACHE_KV?: KVNamespace // Cloudflare KV
RATE_LIMIT?: DurableObjectNamespace // Cloudflare Durable Objects
}
interface RequestContext {
request: Request
env: Env
waitUntil?: (promise: Promise<any>) => void // Background tasks
}
interface ApiResponse<T = any> {
success: boolean
data?: T
error?: string
timestamp: number
}
// ============================================================================
// Configuration
// ============================================================================
const CONFIG = {
CACHE_TTL: 300, // 5 minutes
MAX_REQUEST_SIZE: 1024 * 1024, // 1MB
ALLOWED_ORIGINS: ['https://example.com', 'https://app.example.com'],
RATE_LIMIT: {
REQUESTS: 100,
WINDOW: 60 // seconds
}
}
// ============================================================================
// Main Handler
// ============================================================================
export default {
async fetch(request: Request, env: Env, ctx?: any): Promise<Response> {
const context: RequestContext = {
request,
env,
waitUntil: ctx?.waitUntil
}
try {
// CORS preflight
if (request.method === 'OPTIONS') {
return handleCORS(request)
}
// Validate request
const validation = await validateRequest(request)
if (!validation.valid) {
return jsonResponse({ success: false, error: validation.error }, 400)
}
// Rate limiting (if enabled)
if (env.RATE_LIMIT) {
const rateLimitResult = await checkRateLimit(request, env)
if (!rateLimitResult.allowed) {
return jsonResponse(
{ success: false, error: 'Rate limit exceeded' },
429,
{ 'Retry-After': rateLimitResult.retryAfter.toString() }
)
}
}
// Route request
const response = await routeRequest(context)
// Add CORS headers
return addCORSHeaders(response, request)
} catch (error) {
// Error handling with logging
console.error('Edge function error:', error)
// Background error reporting (non-blocking)
if (context.waitUntil) {
context.waitUntil(reportError(error, request, env))
}
return jsonResponse(
{
success: false,
error: error instanceof Error ? error.message : 'Internal server error'
},
500
)
}
}
}
// ============================================================================
// Request Routing
// ============================================================================
async function routeRequest(ctx: RequestContext): Promise<Response> {
const { request, env } = ctx
const url = new URL(request.url)
const path = url.pathname
// API routes
if (path.startsWith('/api/')) {
return handleApiRequest(request, env)
}
// Health check
if (path === '/health') {
return jsonResponse({ success: true, status: 'healthy' })
}
// Default: proxy to origin or return 404
return jsonResponse({ success: false, error: 'Not found' }, 404)
}
// ============================================================================
// API Handler
// ============================================================================
async function handleApiRequest(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
const endpoint = url.pathname.replace('/api/', '')
switch (endpoint) {
case 'hello':
return handleHello(request)
case 'data':
return handleData(request, env)
case 'proxy':
return handleProxy(request, env)
default:
return jsonResponse({ success: false, error: 'Unknown endpoint' }, 404)
}
}
// ============================================================================
// Endpoint Handlers
// ============================================================================
async function handleHello(request: Request): Promise<Response> {
const url = new URL(request.url)
const name = url.searchParams.get('name') || 'World'
return jsonResponse({
success: true,
data: {
message: `Hello, ${name}!`,
timestamp: Date.now()
}
})
}
async function handleData(request: Request, env: Env): Promise<Response> {
// Try cache first
if (env.CACHE_KV) {
const cached = await env.CACHE_KV.get('data', 'json')
if (cached) {
return jsonResponse({
success: true,
data: cached,
cached: true
})
}
}
// Fetch fresh data
const data = await fetchData(env)
// Cache for future requests (background task)
if (env.CACHE_KV) {
await env.CACHE_KV.put('data', JSON.stringify(data), {
expirationTtl: CONFIG.CACHE_TTL
})
}
return jsonResponse({
success: true,
data,
cached: false
})
}
async function handleProxy(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
const targetUrl = url.searchParams.get('url')
if (!targetUrl) {
return jsonResponse({ success: false, error: 'Missing url parameter' }, 400)
}
// Security: validate target URL
try {
const target = new URL(targetUrl)
if (!['http:', 'https:'].includes(target.protocol)) {
throw new Error('Invalid protocol')
}
} catch {
return jsonResponse({ success: false, error: 'Invalid URL' }, 400)
}
// Proxy request with timeout
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10000) // 10s timeout
try {
const response = await fetch(targetUrl, {
method: request.method,
headers: {
'User-Agent': 'Edge-Proxy/1.0',
'Authorization': request.headers.get('Authorization') || ''
},
signal: controller.signal
})
clearTimeout(timeout)
return new Response(response.body, {
status: response.status,
headers: {
'Content-Type': response.headers.get('Content-Type') || 'application/json'
}
})
} catch (error) {
clearTimeout(timeout)
return jsonResponse({ success: false, error: 'Proxy request failed' }, 502)
}
}
// ============================================================================
// Validation
// ============================================================================
interface ValidationResult {
valid: boolean
error?: string
}
async function validateRequest(request: Request): Promise<ValidationResult> {
// Check request size
const contentLength = request.headers.get('Content-Length')
if (contentLength && parseInt(contentLength) > CONFIG.MAX_REQUEST_SIZE) {
return { valid: false, error: 'Request too large' }
}
// Validate content type for POST/PUT
if (['POST', 'PUT'].includes(request.method)) {
const contentType = request.headers.get('Content-Type')
if (!contentType?.includes('application/json')) {
return { valid: false, error: 'Content-Type must be application/json' }
}
}
return { valid: true }
}
// ============================================================================
// Rate Limiting
// ============================================================================
interface RateLimitResult {
allowed: boolean
retryAfter: number
}
async function checkRateLimit(request: Request, env: Env): Promise<RateLimitResult> {
// Get client identifier (IP or API key)
const clientId = request.headers.get('CF-Connecting-IP') ||
request.headers.get('X-Forwarded-For') ||
'unknown'
const key = `ratelimit:${clientId}`
// Simple KV-based rate limiting
if (env.CACHE_KV) {
const count = await env.CACHE_KV.get(key)
const currentCount = count ? parseInt(count) : 0
if (currentCount >= CONFIG.RATE_LIMIT.REQUESTS) {
return { allowed: false, retryAfter: CONFIG.RATE_LIMIT.WINDOW }
}
await env.CACHE_KV.put(key, (currentCount + 1).toString(), {
expirationTtl: CONFIG.RATE_LIMIT.WINDOW
})
}
return { allowed: true, retryAfter: 0 }
}
// ============================================================================
// CORS Handling
// ============================================================================
function handleCORS(request: Request): Response {
const origin = request.headers.get('Origin') || ''
if (CONFIG.ALLOWED_ORIGINS.includes(origin)) {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400'
}
})
}
return new Response('Forbidden', { status: 403 })
}
function addCORSHeaders(response: Response, request: Request): Response {
const origin = request.headers.get('Origin') || ''
if (CONFIG.ALLOWED_ORIGINS.includes(origin)) {
const newResponse = new Response(response.body, response)
newResponse.headers.set('Access-Control-Allow-Origin', origin)
newResponse.headers.set('Access-Control-Allow-Credentials', 'true')
return newResponse
}
return response
}
// ============================================================================
// Utilities
// ============================================================================
function jsonResponse<T>(
data: ApiResponse<T>,
status: number = 200,
headers: Record<string, string> = {}
): Response {
return new Response(JSON.stringify({ ...data, timestamp: Date.now() }), {
status,
headers: {
'Content-Type': 'application/json',
...headers
}
})
}
async function fetchData(env: Env): Promise<any> {
// Example: fetch from external API
const response = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${env.API_KEY || ''}`
}
})
if (!response.ok) {
throw new Error(`API error: ${response.status}`)
}
return response.json()
}
async function reportError(error: unknown, request: Request, env: Env): Promise<void> {
// Example: send error to logging service (non-blocking)
try {
await fetch('https://logging.example.com/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
url: request.url,
method: request.method,
timestamp: Date.now()
})
})
} catch (logError) {
// Ignore logging errors
console.error('Failed to report error:', logError)
}
}
/**
* Edge Middleware Template (Next.js / Vercel)
*
* Production-ready middleware patterns for authentication, routing,
* A/B testing, geolocation, and request transformation.
*
* Compatible with: Next.js 13+, Vercel Edge Runtime
*/
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
// ============================================================================
// Configuration
// ============================================================================
const CONFIG = {
AUTH: {
PUBLIC_PATHS: ['/login', '/signup', '/forgot-password'],
AUTH_COOKIE: 'auth-token',
SESSION_MAX_AGE: 60 * 60 * 24 * 7 // 7 days
},
AB_TEST: {
ENABLED: true,
COOKIE: 'ab-test-variant',
VARIANTS: ['control', 'variant-a', 'variant-b'] as const,
WEIGHTS: [0.33, 0.33, 0.34] // Must sum to 1.0
},
GEO: {
EU_COUNTRIES: ['DE', 'FR', 'IT', 'ES', 'NL', 'GB', 'PL', 'BE', 'SE', 'AT'],
BLOCKED_COUNTRIES: [] as string[]
},
RATE_LIMIT: {
ENABLED: false, // Use edge config or KV for production
MAX_REQUESTS: 100,
WINDOW_MS: 60000
}
}
// ============================================================================
// Main Middleware
// ============================================================================
export async function middleware(request: NextRequest) {
const url = request.nextUrl.clone()
const path = url.pathname
// 1. Health check (bypass all middleware)
if (path === '/health') {
return NextResponse.json({ status: 'healthy', timestamp: Date.now() })
}
// 2. Geo-blocking
const geoCheck = checkGeolocation(request)
if (!geoCheck.allowed) {
return NextResponse.json(
{ error: 'Service not available in your region' },
{ status: 403 }
)
}
// 3. Rate limiting (if enabled)
if (CONFIG.RATE_LIMIT.ENABLED) {
const rateLimitCheck = await checkRateLimit(request)
if (!rateLimitCheck.allowed) {
return NextResponse.json(
{ error: 'Too many requests' },
{
status: 429,
headers: { 'Retry-After': '60' }
}
)
}
}
// 4. Authentication (for protected routes)
if (requiresAuth(path)) {
const authCheck = await checkAuthentication(request)
if (!authCheck.authenticated) {
return redirectToLogin(url, path)
}
// Add user info to headers for downstream use
const response = NextResponse.next()
response.headers.set('X-User-Id', authCheck.userId || '')
return response
}
// 5. A/B Testing (for experiment paths)
if (isExperimentPath(path)) {
return handleABTest(request, url)
}
// 6. Request rewriting (for localized content)
if (shouldLocalize(path)) {
return handleLocalization(request, url)
}
// 7. Add standard headers
const response = NextResponse.next()
addSecurityHeaders(response)
addGeoHeaders(response, request)
return response
}
// ============================================================================
// Matcher Configuration
// ============================================================================
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization)
* - favicon.ico (favicon file)
* - public assets
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'
]
}
// ============================================================================
// Authentication
// ============================================================================
interface AuthResult {
authenticated: boolean
userId?: string
error?: string
}
function requiresAuth(path: string): boolean {
// Public paths don't require auth
if (CONFIG.AUTH.PUBLIC_PATHS.some(p => path.startsWith(p))) {
return false
}
// Protected paths
return (
path.startsWith('/dashboard') ||
path.startsWith('/api/private') ||
path.startsWith('/account')
)
}
async function checkAuthentication(request: NextRequest): Promise<AuthResult> {
const token = request.cookies.get(CONFIG.AUTH.AUTH_COOKIE)?.value
if (!token) {
return { authenticated: false, error: 'No token' }
}
// Verify JWT at edge (lightweight, no database)
try {
const payload = await verifyJWT(token)
return { authenticated: true, userId: payload.sub }
} catch (error) {
return { authenticated: false, error: 'Invalid token' }
}
}
function redirectToLogin(url: URL, currentPath: string): NextResponse {
const loginUrl = new URL('/login', url.origin)
loginUrl.searchParams.set('redirect', currentPath)
return NextResponse.redirect(loginUrl)
}
// Lightweight JWT verification (edge-compatible)
async function verifyJWT(token: string): Promise<{ sub: string; exp: number }> {
const [headerB64, payloadB64, signatureB64] = token.split('.')
if (!headerB64 || !payloadB64 || !signatureB64) {
throw new Error('Invalid token format')
}
// Decode payload
const payload = JSON.parse(atob(payloadB64))
// Check expiration
if (payload.exp && payload.exp < Date.now() / 1000) {
throw new Error('Token expired')
}
// Note: In production, verify signature with Web Crypto API
// const secret = await getSigningKey()
// const isValid = await crypto.subtle.verify(...)
return payload
}
// ============================================================================
// A/B Testing
// ============================================================================
type ABVariant = typeof CONFIG.AB_TEST.VARIANTS[number]
function isExperimentPath(path: string): boolean {
return (
path.startsWith('/landing') ||
path.startsWith('/pricing') ||
path === '/'
)
}
function handleABTest(request: NextRequest, url: URL): NextResponse {
if (!CONFIG.AB_TEST.ENABLED) {
return NextResponse.next()
}
// Check existing variant cookie
let variant = request.cookies.get(CONFIG.AB_TEST.COOKIE)?.value as ABVariant | undefined
// Assign new variant if none exists
if (!variant || !CONFIG.AB_TEST.VARIANTS.includes(variant)) {
variant = assignVariant()
}
// Rewrite to variant-specific path
if (variant !== 'control') {
url.pathname = `/${variant}${url.pathname}`
}
const response = NextResponse.rewrite(url)
// Set persistent variant cookie
response.cookies.set(CONFIG.AB_TEST.COOKIE, variant, {
maxAge: 60 * 60 * 24 * 30, // 30 days
httpOnly: true,
sameSite: 'strict',
secure: process.env.NODE_ENV === 'production'
})
// Add variant header for analytics
response.headers.set('X-AB-Variant', variant)
return response
}
function assignVariant(): ABVariant {
const random = Math.random()
let cumulative = 0
for (let i = 0; i < CONFIG.AB_TEST.VARIANTS.length; i++) {
cumulative += CONFIG.AB_TEST.WEIGHTS[i]
if (random < cumulative) {
return CONFIG.AB_TEST.VARIANTS[i]
}
}
return CONFIG.AB_TEST.VARIANTS[0] // Fallback
}
// ============================================================================
// Geolocation
// ============================================================================
interface GeoResult {
allowed: boolean
country?: string
reason?: string
}
function checkGeolocation(request: NextRequest): GeoResult {
const country = request.geo?.country || 'US'
// Block specific countries
if (CONFIG.GEO.BLOCKED_COUNTRIES.includes(country)) {
return { allowed: false, country, reason: 'Geo-blocked' }
}
return { allowed: true, country }
}
function shouldLocalize(path: string): boolean {
// Don't localize API routes or static assets
if (path.startsWith('/api') || path.startsWith('/_next')) {
return false
}
return true
}
function handleLocalization(request: NextRequest, url: URL): NextResponse {
const country = request.geo?.country || 'US'
// EU countries get GDPR-compliant version
if (CONFIG.GEO.EU_COUNTRIES.includes(country)) {
url.pathname = `/eu${url.pathname}`
return NextResponse.rewrite(url)
}
return NextResponse.next()
}
function addGeoHeaders(response: NextResponse, request: NextRequest) {
response.headers.set('X-User-Country', request.geo?.country || 'Unknown')
response.headers.set('X-User-City', request.geo?.city || 'Unknown')
response.headers.set('X-User-Region', request.geo?.region || 'Unknown')
response.headers.set('X-User-Latitude', request.geo?.latitude || '0')
response.headers.set('X-User-Longitude', request.geo?.longitude || '0')
}
// ============================================================================
// Rate Limiting (Simple In-Memory)
// ============================================================================
// Note: For production, use Vercel Edge Config, Upstash Redis, or Cloudflare KV
const rateLimitStore = new Map<string, { count: number; resetAt: number }>()
interface RateLimitResult {
allowed: boolean
retryAfter?: number
}
async function checkRateLimit(request: NextRequest): Promise<RateLimitResult> {
const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown'
const key = `ratelimit:${ip}`
const now = Date.now()
const existing = rateLimitStore.get(key)
// Reset if window expired
if (!existing || existing.resetAt < now) {
rateLimitStore.set(key, {
count: 1,
resetAt: now + CONFIG.RATE_LIMIT.WINDOW_MS
})
return { allowed: true }
}
// Increment count
existing.count++
if (existing.count > CONFIG.RATE_LIMIT.MAX_REQUESTS) {
const retryAfter = Math.ceil((existing.resetAt - now) / 1000)
return { allowed: false, retryAfter }
}
return { allowed: true }
}
// ============================================================================
// Security Headers
// ============================================================================
function addSecurityHeaders(response: NextResponse) {
// Prevent clickjacking
response.headers.set('X-Frame-Options', 'DENY')
// Enable XSS protection
response.headers.set('X-Content-Type-Options', 'nosniff')
// Referrer policy
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
// Permissions policy
response.headers.set(
'Permissions-Policy',
'camera=(), microphone=(), geolocation=()'
)
// Content Security Policy (basic example)
response.headers.set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';"
)
}
// ============================================================================
// Request Rewriting Examples
// ============================================================================
// Feature flag routing (requires Vercel Edge Config)
export async function featureFlagMiddleware(request: NextRequest) {
// Uncomment when using @vercel/edge-config
// import { get } from '@vercel/edge-config'
//
// const enableNewUI = await get('enable_new_ui')
//
// if (enableNewUI) {
// const url = request.nextUrl.clone()
// url.pathname = `/new-ui${url.pathname}`
// return NextResponse.rewrite(url)
// }
return NextResponse.next()
}
// Mobile detection and routing
export function mobileMiddleware(request: NextRequest) {
const userAgent = request.headers.get('user-agent') || ''
const isMobile = /mobile|android|iphone|ipad|phone/i.test(userAgent)
if (isMobile && !request.nextUrl.pathname.startsWith('/mobile')) {
const url = request.nextUrl.clone()
url.pathname = `/mobile${url.pathname}`
return NextResponse.rewrite(url)
}
return NextResponse.next()
}
// Custom domain routing
export function domainMiddleware(request: NextRequest) {
const hostname = request.headers.get('host') || ''
if (hostname.startsWith('app.')) {
const url = request.nextUrl.clone()
url.pathname = `/app${url.pathname}`
return NextResponse.rewrite(url)
}
if (hostname.startsWith('api.')) {
const url = request.nextUrl.clone()
url.pathname = `/api${url.pathname}`
return NextResponse.rewrite(url)
}
return NextResponse.next()
}