Bỏ qua

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;
}

Performance Tips

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

Performance

  • [ ] 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

  1. Use edge runtime for simple, fast operations
  2. Delegate heavy work to Queue
  3. Cache with KV
  4. Stream large responses
  5. Handle errors properly
  6. Use type-safe environment

Don'ts

  1. Don't use Node.js APIs
  2. Don't rely on global state
  3. Don't load large files into memory
  4. Don't do long-running operations
  5. Don't forget CORS
  6. Don't create clients on every request

Q&A

  1. Pitfall nào bạn đã gặp phải?
  2. Best practice nào khó implement nhất?
  3. Có performance concerns nào?