chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,809 @@
|
||||
---
|
||||
allowed-tools: Read, Write, Edit
|
||||
argument-hint: [function-name] [--auth] [--geo] [--transform] [--proxy]
|
||||
description: Generate optimized Vercel Edge Functions with geolocation, authentication, and data transformation
|
||||
---
|
||||
|
||||
## Vercel Edge Function Generator
|
||||
|
||||
**Function Name**: $ARGUMENTS
|
||||
|
||||
## Current Project Analysis
|
||||
|
||||
### Project Structure
|
||||
- Vercel config: @vercel.json (if exists)
|
||||
- Next.js config: @next.config.js
|
||||
- API routes: @app/api/ or @pages/api/
|
||||
- Middleware: @middleware.ts (if exists)
|
||||
|
||||
### Framework Detection
|
||||
- Package.json: @package.json
|
||||
- TypeScript config: @tsconfig.json (if exists)
|
||||
- Environment variables: @.env.local (if exists)
|
||||
|
||||
## Edge Function Implementation Strategy
|
||||
|
||||
### 1. File Structure Creation
|
||||
Generate comprehensive edge function structure:
|
||||
```
|
||||
api/edge/[function-name]/
|
||||
├── index.ts # Main edge function
|
||||
├── types.ts # TypeScript types
|
||||
├── utils.ts # Utility functions
|
||||
├── config.ts # Configuration
|
||||
└── __tests__/
|
||||
└── [function-name].test.ts # Unit tests
|
||||
```
|
||||
|
||||
### 2. Base Edge Function Template
|
||||
```typescript
|
||||
// api/edge/[function-name]/index.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Get geolocation data
|
||||
const country = request.geo?.country || 'Unknown';
|
||||
const city = request.geo?.city || 'Unknown';
|
||||
const region = request.geo?.region || 'Unknown';
|
||||
|
||||
// Get request metadata
|
||||
const ip = request.headers.get('x-forwarded-for') || 'Unknown';
|
||||
const userAgent = request.headers.get('user-agent') || 'Unknown';
|
||||
const referer = request.headers.get('referer') || 'Unknown';
|
||||
|
||||
// Process request
|
||||
const result = await processRequest({
|
||||
geo: { country, city, region },
|
||||
ip,
|
||||
userAgent,
|
||||
referer,
|
||||
url: request.url,
|
||||
});
|
||||
|
||||
return NextResponse.json(result, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Edge-Location': region,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Edge function error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
|
||||
// Validate request body
|
||||
const validationResult = validateRequestBody(body);
|
||||
if (!validationResult.valid) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request body', details: validationResult.errors },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Process POST request
|
||||
const result = await processPostRequest(body, request);
|
||||
|
||||
return NextResponse.json(result, {
|
||||
status: 201,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Edge function POST error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function processRequest(metadata: RequestMetadata): Promise<any> {
|
||||
// Implement your edge function logic here
|
||||
return {
|
||||
message: 'Edge function executed successfully',
|
||||
metadata,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function processPostRequest(body: any, request: NextRequest): Promise<any> {
|
||||
// Implement POST logic here
|
||||
return {
|
||||
message: 'POST processed successfully',
|
||||
data: body,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function validateRequestBody(body: any): ValidationResult {
|
||||
// Implement validation logic
|
||||
return { valid: true, errors: [] };
|
||||
}
|
||||
|
||||
interface RequestMetadata {
|
||||
geo: {
|
||||
country: string;
|
||||
city: string;
|
||||
region: string;
|
||||
};
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
referer: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
```
|
||||
|
||||
## Specialized Edge Function Types
|
||||
|
||||
### 1. Geolocation-Based Content Delivery
|
||||
```typescript
|
||||
// api/edge/geo-content/index.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
interface ContentConfig {
|
||||
[country: string]: {
|
||||
currency: string;
|
||||
language: string;
|
||||
content: string;
|
||||
pricing: number;
|
||||
};
|
||||
}
|
||||
|
||||
const contentConfig: ContentConfig = {
|
||||
'US': {
|
||||
currency: 'USD',
|
||||
language: 'en-US',
|
||||
content: 'Welcome to our US store!',
|
||||
pricing: 99.99,
|
||||
},
|
||||
'GB': {
|
||||
currency: 'GBP',
|
||||
language: 'en-GB',
|
||||
content: 'Welcome to our UK store!',
|
||||
pricing: 79.99,
|
||||
},
|
||||
'DE': {
|
||||
currency: 'EUR',
|
||||
language: 'de-DE',
|
||||
content: 'Willkommen in unserem deutschen Shop!',
|
||||
pricing: 89.99,
|
||||
},
|
||||
};
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const country = request.geo?.country || 'US';
|
||||
const config = contentConfig[country] || contentConfig['US'];
|
||||
|
||||
// Add region-specific headers
|
||||
const response = NextResponse.json({
|
||||
country,
|
||||
...config,
|
||||
edgeLocation: request.geo?.region,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
response.headers.set('Cache-Control', 'public, s-maxage=3600, stale-while-revalidate=86400');
|
||||
response.headers.set('Vary', 'Accept-Language, CloudFront-Viewer-Country');
|
||||
response.headers.set('Content-Language', config.language);
|
||||
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Authentication Edge Function
|
||||
```typescript
|
||||
// api/edge/auth-check/index.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { jwtVerify } from 'jose';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
const JWT_SECRET = new TextEncoder().encode(
|
||||
process.env.JWT_SECRET || 'your-secret-key'
|
||||
);
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
// Extract token from header or cookie
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const cookieToken = request.cookies.get('auth-token')?.value;
|
||||
|
||||
const token = authHeader?.replace('Bearer ', '') || cookieToken;
|
||||
|
||||
if (!token) {
|
||||
return NextResponse.json(
|
||||
{ error: 'No token provided', authenticated: false },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Verify JWT token
|
||||
const { payload } = await jwtVerify(token, JWT_SECRET);
|
||||
|
||||
// Return user info
|
||||
return NextResponse.json({
|
||||
authenticated: true,
|
||||
user: {
|
||||
id: payload.sub,
|
||||
email: payload.email,
|
||||
role: payload.role,
|
||||
exp: payload.exp,
|
||||
},
|
||||
location: {
|
||||
country: request.geo?.country,
|
||||
city: request.geo?.city,
|
||||
},
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Auth verification failed:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid token', authenticated: false },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { username, password } = await request.json();
|
||||
|
||||
// Validate credentials (implement your logic)
|
||||
const user = await validateCredentials(username, password);
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid credentials' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
const token = await generateJWT(user);
|
||||
|
||||
const response = NextResponse.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
},
|
||||
});
|
||||
|
||||
// Set secure cookie
|
||||
response.cookies.set('auth-token', token, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: 24 * 60 * 60, // 24 hours
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Authentication error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Authentication failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function validateCredentials(username: string, password: string) {
|
||||
// Implement credential validation
|
||||
// This would typically involve database lookup
|
||||
return null; // Placeholder
|
||||
}
|
||||
|
||||
async function generateJWT(user: any): Promise<string> {
|
||||
// Implement JWT generation
|
||||
// This would use a proper JWT library
|
||||
return 'jwt-token'; // Placeholder
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Data Transformation Edge Function
|
||||
```typescript
|
||||
// api/edge/transform/index.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
interface TransformConfig {
|
||||
format: 'json' | 'xml' | 'csv';
|
||||
fields?: string[];
|
||||
transforms?: Record<string, (value: any) => any>;
|
||||
}
|
||||
|
||||
const transformers = {
|
||||
// Currency conversion
|
||||
currency: (value: number, targetCurrency: string = 'USD') => {
|
||||
const rates = { USD: 1, EUR: 0.85, GBP: 0.73 };
|
||||
return value * (rates[targetCurrency as keyof typeof rates] || 1);
|
||||
},
|
||||
|
||||
// Date formatting
|
||||
date: (value: string, format: string = 'ISO') => {
|
||||
const date = new Date(value);
|
||||
if (format === 'ISO') return date.toISOString();
|
||||
if (format === 'US') return date.toLocaleDateString('en-US');
|
||||
return date.toString();
|
||||
},
|
||||
|
||||
// Text formatting
|
||||
text: (value: string, caseType: string = 'lower') => {
|
||||
if (caseType === 'upper') return value.toUpperCase();
|
||||
if (caseType === 'title') return value.replace(/\w\S*/g, txt =>
|
||||
txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
|
||||
);
|
||||
return value.toLowerCase();
|
||||
},
|
||||
};
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { data, config }: { data: any; config: TransformConfig } = await request.json();
|
||||
|
||||
if (!data || !config) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing data or config' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Apply transformations
|
||||
const transformedData = await transformData(data, config, request);
|
||||
|
||||
// Format output based on requested format
|
||||
const output = await formatOutput(transformedData, config.format);
|
||||
|
||||
const response = new NextResponse(output, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': getContentType(config.format),
|
||||
'Cache-Control': 'public, s-maxage=300',
|
||||
},
|
||||
});
|
||||
|
||||
return response;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Transform error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Transformation failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function transformData(data: any, config: TransformConfig, request: NextRequest) {
|
||||
const country = request.geo?.country || 'US';
|
||||
|
||||
// Apply field filtering if specified
|
||||
if (config.fields && Array.isArray(data)) {
|
||||
data = data.map(item => {
|
||||
const filtered: any = {};
|
||||
config.fields!.forEach(field => {
|
||||
if (item.hasOwnProperty(field)) {
|
||||
filtered[field] = item[field];
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply custom transforms
|
||||
if (config.transforms) {
|
||||
Object.entries(config.transforms).forEach(([field, transformFunc]) => {
|
||||
if (Array.isArray(data)) {
|
||||
data = data.map(item => ({
|
||||
...item,
|
||||
[field]: transformFunc(item[field]),
|
||||
}));
|
||||
} else if (data.hasOwnProperty(field)) {
|
||||
data[field] = transformFunc(data[field]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add geo context
|
||||
return {
|
||||
...data,
|
||||
_meta: {
|
||||
transformedAt: new Date().toISOString(),
|
||||
location: country,
|
||||
edgeRegion: request.geo?.region,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function formatOutput(data: any, format: string): Promise<string> {
|
||||
switch (format) {
|
||||
case 'xml':
|
||||
return jsonToXml(data);
|
||||
case 'csv':
|
||||
return jsonToCsv(data);
|
||||
case 'json':
|
||||
default:
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
function getContentType(format: string): string {
|
||||
switch (format) {
|
||||
case 'xml': return 'application/xml';
|
||||
case 'csv': return 'text/csv';
|
||||
case 'json':
|
||||
default: return 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
function jsonToXml(data: any): string {
|
||||
// Simple XML conversion (implement proper XML library for production)
|
||||
return `<?xml version="1.0" encoding="UTF-8"?><root>${JSON.stringify(data)}</root>`;
|
||||
}
|
||||
|
||||
function jsonToCsv(data: any): string {
|
||||
// Simple CSV conversion (implement proper CSV library for production)
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
const headers = Object.keys(data[0]);
|
||||
const rows = data.map(row => headers.map(header => row[header] || '').join(','));
|
||||
return [headers.join(','), ...rows].join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Proxy and Cache Edge Function
|
||||
```typescript
|
||||
// api/edge/proxy/index.ts
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
interface ProxyConfig {
|
||||
targetUrl: string;
|
||||
cacheTime: number;
|
||||
headers?: Record<string, string>;
|
||||
transformResponse?: boolean;
|
||||
}
|
||||
|
||||
const proxyConfigs: Record<string, ProxyConfig> = {
|
||||
'api': {
|
||||
targetUrl: 'https://jsonplaceholder.typicode.com',
|
||||
cacheTime: 300, // 5 minutes
|
||||
headers: {
|
||||
'User-Agent': 'Vercel-Edge-Proxy/1.0',
|
||||
},
|
||||
},
|
||||
'cdn': {
|
||||
targetUrl: 'https://cdn.example.com',
|
||||
cacheTime: 3600, // 1 hour
|
||||
transformResponse: false,
|
||||
},
|
||||
};
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const proxyType = url.searchParams.get('type') || 'api';
|
||||
const targetPath = url.searchParams.get('path') || '';
|
||||
|
||||
const config = proxyConfigs[proxyType];
|
||||
if (!config) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid proxy type' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build target URL
|
||||
const targetUrl = `${config.targetUrl}${targetPath}`;
|
||||
|
||||
// Check cache first (simplified - use proper cache in production)
|
||||
const cacheKey = `proxy:${targetUrl}`;
|
||||
|
||||
// Make request to target
|
||||
const response = await fetch(targetUrl, {
|
||||
headers: {
|
||||
...config.headers,
|
||||
'X-Forwarded-For': request.headers.get('x-forwarded-for') || '',
|
||||
'X-Real-IP': request.headers.get('x-real-ip') || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Upstream server error' },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
let data;
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
if (contentType.includes('application/json')) {
|
||||
data = await response.json();
|
||||
|
||||
// Transform response if configured
|
||||
if (config.transformResponse) {
|
||||
data = await transformProxyResponse(data, request);
|
||||
}
|
||||
|
||||
return NextResponse.json(data, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Cache-Control': `public, s-maxage=${config.cacheTime}, stale-while-revalidate=${config.cacheTime * 2}`,
|
||||
'X-Proxy-Cache': 'MISS',
|
||||
'X-Edge-Location': request.geo?.region || 'unknown',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// For non-JSON responses, pass through
|
||||
const blob = await response.blob();
|
||||
|
||||
return new NextResponse(blob, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': `public, s-maxage=${config.cacheTime}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Proxy error:', error);
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: 'Proxy request failed' },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function transformProxyResponse(data: any, request: NextRequest) {
|
||||
// Add geo context to proxied data
|
||||
return {
|
||||
...data,
|
||||
_proxy: {
|
||||
timestamp: new Date().toISOString(),
|
||||
location: request.geo?.country,
|
||||
region: request.geo?.region,
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Edge Function Utilities
|
||||
|
||||
### 1. Configuration Management
|
||||
```typescript
|
||||
// api/edge/[function-name]/config.ts
|
||||
export interface EdgeFunctionConfig {
|
||||
cacheTime: number;
|
||||
rateLimit: {
|
||||
requests: number;
|
||||
windowMs: number;
|
||||
};
|
||||
geo: {
|
||||
enabled: boolean;
|
||||
restrictedCountries?: string[];
|
||||
};
|
||||
security: {
|
||||
corsOrigins: string[];
|
||||
requireAuth: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultConfig: EdgeFunctionConfig = {
|
||||
cacheTime: 300, // 5 minutes
|
||||
rateLimit: {
|
||||
requests: 100,
|
||||
windowMs: 60000, // 1 minute
|
||||
},
|
||||
geo: {
|
||||
enabled: true,
|
||||
},
|
||||
security: {
|
||||
corsOrigins: ['*'],
|
||||
requireAuth: false,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Utility Functions
|
||||
```typescript
|
||||
// api/edge/[function-name]/utils.ts
|
||||
export function getClientIP(request: NextRequest): string {
|
||||
return request.headers.get('x-forwarded-for') ||
|
||||
request.headers.get('x-real-ip') ||
|
||||
request.ip ||
|
||||
'unknown';
|
||||
}
|
||||
|
||||
export function generateCacheKey(request: NextRequest, suffix?: string): string {
|
||||
const url = new URL(request.url);
|
||||
const baseKey = `${url.pathname}${url.search}`;
|
||||
return suffix ? `${baseKey}:${suffix}` : baseKey;
|
||||
}
|
||||
|
||||
export function createCorsResponse(
|
||||
data: any,
|
||||
origins: string[] = ['*']
|
||||
): NextResponse {
|
||||
const response = NextResponse.json(data);
|
||||
|
||||
response.headers.set('Access-Control-Allow-Origin', origins.join(', '));
|
||||
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
||||
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
export function validateGeoRestrictions(
|
||||
request: NextRequest,
|
||||
restrictedCountries: string[] = []
|
||||
): boolean {
|
||||
const country = request.geo?.country;
|
||||
return !country || !restrictedCountries.includes(country);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Testing Framework
|
||||
```typescript
|
||||
// api/edge/[function-name]/__tests__/[function-name].test.ts
|
||||
import { NextRequest } from 'next/server';
|
||||
import { GET, POST } from '../index';
|
||||
|
||||
// Mock geo data
|
||||
const createMockRequest = (url: string, options: any = {}) => {
|
||||
const request = new NextRequest(url, options);
|
||||
|
||||
// Mock geo property
|
||||
Object.defineProperty(request, 'geo', {
|
||||
value: {
|
||||
country: 'US',
|
||||
city: 'New York',
|
||||
region: 'us-east-1',
|
||||
},
|
||||
});
|
||||
|
||||
return request;
|
||||
};
|
||||
|
||||
describe('Edge Function', () => {
|
||||
describe('GET requests', () => {
|
||||
it('should return geo-based content', async () => {
|
||||
const request = createMockRequest('http://localhost:3000/api/edge/test');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.metadata.geo.country).toBe('US');
|
||||
});
|
||||
|
||||
it('should handle missing geo data', async () => {
|
||||
const request = new NextRequest('http://localhost:3000/api/edge/test');
|
||||
const response = await GET(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(data.metadata.geo.country).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST requests', () => {
|
||||
it('should validate request body', async () => {
|
||||
const request = createMockRequest('http://localhost:3000/api/edge/test', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ invalid: 'data' }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
const data = await response.json();
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(data.error).toBe('Invalid request body');
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Performance and Optimization
|
||||
|
||||
### 1. Response Optimization
|
||||
```typescript
|
||||
// Optimize responses for edge performance
|
||||
export function optimizeResponse(data: any, request: NextRequest): NextResponse {
|
||||
const response = NextResponse.json(data);
|
||||
|
||||
// Set appropriate cache headers
|
||||
const cacheTime = getCacheTime(request.url);
|
||||
response.headers.set(
|
||||
'Cache-Control',
|
||||
`public, s-maxage=${cacheTime}, stale-while-revalidate=${cacheTime * 2}`
|
||||
);
|
||||
|
||||
// Add compression hints
|
||||
response.headers.set('Content-Encoding', 'gzip');
|
||||
|
||||
// Add performance headers
|
||||
response.headers.set('X-Edge-Location', request.geo?.region || 'unknown');
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function getCacheTime(url: string): number {
|
||||
// Dynamic cache time based on URL patterns
|
||||
if (url.includes('/static/')) return 3600; // 1 hour
|
||||
if (url.includes('/api/')) return 60; // 1 minute
|
||||
return 300; // 5 minutes default
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Error Handling
|
||||
```typescript
|
||||
export function createErrorResponse(
|
||||
error: unknown,
|
||||
request: NextRequest
|
||||
): NextResponse {
|
||||
console.error('Edge function error:', error);
|
||||
|
||||
// Log error with context
|
||||
const errorContext = {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
country: request.geo?.country,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
|
||||
// Return appropriate error response
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Internal server error',
|
||||
requestId: generateRequestId(),
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
headers: {
|
||||
'X-Error-Context': JSON.stringify(errorContext),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function generateRequestId(): string {
|
||||
return Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
```
|
||||
|
||||
Generate comprehensive edge function implementation with the requested features, proper TypeScript types, error handling, and optimization patterns.
|
||||
Reference in New Issue
Block a user