Best Practices & Pitfalls
Best Practices
1. Sử dụng đúng Runtime
// ✅ GOOD: Explicit runtime declaration
// app/api/fast/route.ts
export const runtime = 'edge';
export async function GET() {
// Simple, fast operations
return Response.json({ ok: true });
}
// ✅ GOOD: Heavy work → delegate
// app/api/process/route.ts
export const runtime = 'edge';
export async function POST(request: Request) {
const data = await request.json();
// Delegate heavy work to Queue
await env.QUEUE.send({ action: 'process', data });
// Return immediately
return Response.json({ status: 'queued' });
}
2. Caching Strategy
// ✅ GOOD: Cache với KV
export async function GET(request: Request) {
const { env } = getRequestContext();
const cacheKey = new URL(request.url).pathname;
// Check cache first
const cached = await env.KV.get(cacheKey, 'json');
if (cached) {
return Response.json(cached, {
headers: { 'X-Cache': 'HIT' },
});
}
// Fetch fresh data
const data = await fetchData();
// Cache for 1 hour
await env.KV.put(cacheKey, JSON.stringify(data), {
expirationTtl: 3600,
});
return Response.json(data, {
headers: { 'X-Cache': 'MISS' },
});
}
3. Error Handling
// ✅ GOOD: Proper error handling
export async function GET(request: Request) {
try {
const data = await riskyOperation();
return Response.json(data);
} catch (error) {
// Log error (will appear in wrangler tail)
console.error('Operation failed:', error);
// Return appropriate error response
if (error instanceof ValidationError) {
return Response.json(
{ error: error.message },
{ status: 400 }
);
}
if (error instanceof AuthError) {
return Response.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// Generic error (don't expose details)
return Response.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
4. Environment Variables
// ✅ GOOD: Type-safe env access
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
SUPABASE_URL: z.string().url(),
SUPABASE_ANON_KEY: z.string().min(1),
ENVIRONMENT: z.enum(['development', 'staging', 'production']),
});
export function validateEnv(env: unknown) {
return envSchema.parse(env);
}
// Usage
const env = validateEnv(getRequestContext().env);
5. Streaming Responses
// ✅ GOOD: Stream large responses
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of fetchLargeData()) {
controller.enqueue(encoder.encode(JSON.stringify(chunk) + '\n'));
}
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'application/x-ndjson',
'Transfer-Encoding': 'chunked',
},
});
}
Common Pitfalls
Pitfall 1: Using Node.js APIs
// ❌ BAD: Node.js APIs don't work
import fs from 'fs';
import path from 'path';
export async function GET() {
const data = fs.readFileSync(path.join(__dirname, 'data.json'));
return Response.json(JSON.parse(data));
}
// ✅ GOOD: Use fetch or KV
export async function GET() {
const { env } = getRequestContext();
// Option 1: From KV
const data = await env.KV.get('data', 'json');
// Option 2: From R2
const object = await env.R2.get('data.json');
const data = await object.json();
// Option 3: Embed in code (for small static data)
const data = { key: 'value' };
return Response.json(data);
}
Pitfall 2: Global State
// ❌ BAD: Global state doesn't persist
let requestCount = 0;
const cache = new Map();
export async function GET() {
requestCount++; // Resets randomly!
cache.set('key', 'value'); // Lost on cold start!
return Response.json({ count: requestCount });
}
// ✅ GOOD: Use external state
export async function GET() {
const { env } = getRequestContext();
// Atomic increment in KV (with limitations)
// Better: Use Supabase for counters
const { data } = await supabase.rpc('increment_counter', { id: 'main' });
return Response.json({ count: data.value });
}
Pitfall 3: Large Payloads
// ❌ BAD: Loading entire file into memory
export async function POST(request: Request) {
const file = await request.arrayBuffer(); // 500MB file!
const processed = processFile(file); // Memory exceeded!
return Response.json(processed);
}
// ✅ GOOD: Stream or limit size
export async function POST(request: Request) {
// Check size first
const contentLength = request.headers.get('content-length');
if (parseInt(contentLength || '0') > 10 * 1024 * 1024) {
return Response.json(
{ error: 'File too large. Max 10MB.' },
{ status: 413 }
);
}
// Or stream to R2/Supabase Storage
await env.R2.put('uploads/' + filename, request.body);
return Response.json({ status: 'uploaded' });
}
Pitfall 4: Blocking Operations
// ❌ BAD: Long-running sync operation
export async function POST(request: Request) {
const data = await request.json();
// This might timeout!
for (let i = 0; i < 1000000; i++) {
await heavyOperation(i);
}
return Response.json({ done: true });
}
// ✅ GOOD: Queue for background processing
export async function POST(request: Request) {
const data = await request.json();
// Enqueue job
await env.QUEUE.send({
type: 'heavy-operation',
payload: data,
});
// Return immediately
return Response.json({
status: 'processing',
message: 'Job queued for processing',
});
}
Pitfall 5: Wrong Supabase Client
// ❌ BAD: Using Node.js specific client
import { createClient } from '@supabase/supabase-js/node';
// ❌ BAD: Creating client on every request
export async function GET() {
const supabase = createClient(url, key); // Expensive!
const { data } = await supabase.from('users').select();
return Response.json(data);
}
// ✅ GOOD: Singleton pattern
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
let supabase: ReturnType<typeof createClient> | null = null;
export function getSupabase(env: CloudflareEnv) {
if (!supabase) {
supabase = createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY, {
auth: { persistSession: false },
});
}
return supabase;
}
// Usage
export async function GET() {
const { env } = getRequestContext();
const supabase = getSupabase(env);
const { data } = await supabase.from('users').select();
return Response.json(data);
}
Pitfall 6: CORS Issues
// ❌ BAD: Forgot CORS headers
export async function GET() {
return Response.json({ data: 'test' });
// Browser: CORS error!
}
// ✅ GOOD: Proper CORS handling
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
export async function OPTIONS() {
return new Response(null, { headers: corsHeaders });
}
export async function GET() {
return Response.json(
{ data: 'test' },
{ headers: corsHeaders }
);
}
// ✅ BETTER: Use middleware
// middleware.ts
export function middleware(request: NextRequest) {
const response = NextResponse.next();
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
return response;
}
1. Minimize cold starts
// Keep worker warm với scheduled ping
// wrangler.toml
[triggers]
crons = ["*/5 * * * *"] # Ping every 5 minutes
2. Use edge caching
// Return cacheable responses
return new Response(data, {
headers: {
'Cache-Control': 'public, max-age=3600',
'CDN-Cache-Control': 'public, max-age=86400',
},
});
3. Parallelize requests
// ✅ Parallel fetching
const [users, posts, comments] = await Promise.all([
supabase.from('users').select(),
supabase.from('posts').select(),
supabase.from('comments').select(),
]);
Checklist trước Production
Code Quality
- [ ] Tất cả routes đều handle errors
- [ ] Không có global state
- [ ] Đúng runtime cho mỗi route
- [ ] Type-safe environment variables
- [ ] Có caching strategy
- [ ] Không có blocking operations
- [ ] Large payloads được stream hoặc reject
Security
- [ ] CORS configured properly
- [ ] Secrets không hardcode
- [ ] Rate limiting (nếu cần)
Deployment
- [ ] Multi-environment setup
- [ ] CI/CD pipeline
- [ ] Rollback strategy
- [ ] Monitoring enabled
Tổng kết
Do's
- Use edge runtime for simple, fast operations
- Delegate heavy work to Queue
- Cache with KV
- Stream large responses
- Handle errors properly
- Use type-safe environment
Don'ts
- Don't use Node.js APIs
- Don't rely on global state
- Don't load large files into memory
- Don't do long-running operations
- Don't forget CORS
- Don't create clients on every request
Q&A
- Pitfall nào bạn đã gặp phải?
- Best practice nào khó implement nhất?
- Có performance concerns nào?