article

Hono + Cloudflare Workers ที่ทำให้ API เร็วจนไม่อยากเชื่อ

16 min read

วันที่เจอปัญหา Latency

วันหนึ่งได้รายงานจาก product team: “API ช้ามาก! User จาก Singapore เรียก API แล้วรอ 800ms” 😱

ตอนนั้นใช้ Express.js deploy อยู่ที่ AWS us-east-1:

// server.js - Express.js ที่ deploy ในอเมริกา
const express = require('express');
const app = express();

app.get('/api/user/:id', async (req, res) => {
  // Query database ใช้เวลา ~50ms
  const user = await db.users.findById(req.params.id);
  
  // Response time ทั้งหมด:
  // Network latency (SG -> US): ~400ms
  // Database query: ~50ms  
  // Processing: ~10ms
  // Network latency (US -> SG): ~400ms
  // Total: ~860ms 🐌
  
  res.json(user);
});

ปัญหาคือ: ไม่ว่าจะ optimize code ยังไง network latency จาก Singapore ไป US มันไม่มีทางเร็วขึ้นได้!

แล้วก็มาได้ยินเรื่อง “Edge Computing” และ Cloudflare Workers 🌟

การรู้จัก Cloudflare Workers ครั้งแรก

Edge Computing คือการรัน code ใกล้ๆ กับ user แทนที่จะรันใน data center กลาง

Cloudflare Workers คือ serverless platform ที่รันโค้ดใน 300+ locations ทั่วโลก!

Concept ที่เจ๋ง:

  • Code รันใน edge location ที่ใกล้ user ที่สุด
  • Cold start ต่ำกว่า 1ms
  • Auto-scaling แบบ global
  • จ่ายแค่ตาม requests

First Attempt กับ Cloudflare Workers

// worker.js - ครั้งแรกที่ลอง
addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);
  
  if (url.pathname === '/api/hello') {
    return new Response('Hello from the edge!', {
      headers: { 'Content-Type': 'text/plain' },
    });
  }
  
  return new Response('Not found', { status: 404 });
}

ปัญหาที่เจอ:

  • Syntax แปลกๆ ไม่เหมือน Express
  • Routing ต้องเขียนเอง
  • Middleware concept ไม่มี
  • TypeScript support ไม่ดี

ผลลัพธ์: ใช้เวลา 2 วันแต่งได้แค่ Hello World! 😅

การค้นพบ Hono Framework

พอเจอ Hono รู้เลยว่า “นี่แหละที่หาอยู่!”

Hono คืออะไร:

  • Fast web framework สำหรับ edge runtimes
  • Syntax คล้าย Express.js
  • TypeScript first
  • รองรับ Cloudflare Workers, Bun, Deno, Node.js
  • Super lightweight (~13KB)

ลองใช้ Hono ครั้งแรก

// index.ts - Hono version
import { Hono } from 'hono';

const app = new Hono();

app.get('/', (c) => {
  return c.text('Hello from Hono on the edge! 🔥');
});

app.get('/api/user/:id', async (c) => {
  const userId = c.req.param('id');
  
  // Simulate user data (ยังไม่มี database)
  const user = {
    id: userId,
    name: `User ${userId}`,
    location: c.req.header('cf-ipcountry') || 'Unknown'
  };
  
  return c.json(user);
});

export default app;

Deploy แค่:

npm install hono
npx wrangler dev      # Local development
npx wrangler deploy   # Deploy to edge

ผลลัพธ์: API ที่เคยใช้เวลา 800ms กลายเป็น 80ms! 🚀

เคสจริง: URL Shortener API

ต่อมาลองทำโปรเจคจริงๆ: URL Shortener ที่ต้องเร็วมาก

Architecture ที่ออกแบบ

// types.ts
export interface ShortUrl {
  id: string;
  originalUrl: string;
  shortCode: string;
  clicks: number;
  createdAt: string;
  expiresAt?: string;
}

export interface CreateUrlRequest {
  url: string;
  customCode?: string;
  expiresIn?: number; // seconds
}

Main Application

// index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

const app = new Hono();

// Middleware
app.use('*', logger());
app.use('/api/*', cors({
  origin: ['https://myapp.com', 'http://localhost:3000'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
}));

// Validation schemas
const createUrlSchema = z.object({
  url: z.string().url('Invalid URL format'),
  customCode: z.string().min(3).max(10).optional(),
  expiresIn: z.number().min(60).max(31536000).optional(), // 1 min to 1 year
});

// Routes
app.get('/', (c) => {
  return c.html(`
    <html>
      <body>
        <h1>URL Shortener API 🔗</h1>
        <p>Running on Cloudflare Workers at the edge!</p>
        <p>Location: ${c.req.header('cf-ipcountry')}</p>
        <p>Datacenter: ${c.req.header('cf-colo')}</p>
      </body>
    </html>
  `);
});

// Create short URL
app.post('/api/shorten', 
  zValidator('json', createUrlSchema),
  async (c) => {
    const { url, customCode, expiresIn } = c.req.valid('json');
    
    try {
      // Generate short code if not provided
      const shortCode = customCode || generateShortCode();
      
      // Check if custom code already exists
      if (customCode) {
        const existing = await getUrlByCode(shortCode);
        if (existing) {
          return c.json({ 
            error: 'Custom code already exists',
            code: 'CODE_EXISTS'
          }, 409);
        }
      }
      
      // Save to KV storage
      const shortUrl: ShortUrl = {
        id: crypto.randomUUID(),
        originalUrl: url,
        shortCode,
        clicks: 0,
        createdAt: new Date().toISOString(),
        expiresAt: expiresIn 
          ? new Date(Date.now() + expiresIn * 1000).toISOString()
          : undefined
      };
      
      await saveUrl(shortUrl);
      
      // Analytics event
      c.executionCtx.waitUntil(
        trackEvent('url_created', {
          shortCode,
          country: c.req.header('cf-ipcountry'),
          userAgent: c.req.header('user-agent')
        })
      );
      
      return c.json({
        shortUrl: `https://short.ly/${shortCode}`,
        shortCode,
        originalUrl: url,
        expiresAt: shortUrl.expiresAt
      }, 201);
      
    } catch (error) {
      console.error('Error creating short URL:', error);
      return c.json({ 
        error: 'Failed to create short URL',
        code: 'CREATION_FAILED'
      }, 500);
    }
  }
);

// Redirect endpoint
app.get('/:code', async (c) => {
  const shortCode = c.req.param('code');
  
  try {
    const urlData = await getUrlByCode(shortCode);
    
    if (!urlData) {
      return c.html(`
        <html><body>
          <h1>404 - Short URL not found 😞</h1>
          <p>The link you're looking for doesn't exist or has expired.</p>
        </body></html>
      `, 404);
    }
    
    // Check expiration
    if (urlData.expiresAt && new Date() > new Date(urlData.expiresAt)) {
      return c.html(`
        <html><body>
          <h1>410 - Link Expired ⏰</h1>
          <p>This short URL has expired.</p>
        </body></html>
      `, 410);
    }
    
    // Increment click count (async, don't wait)
    c.executionCtx.waitUntil(incrementClicks(shortCode));
    
    // Track click analytics
    c.executionCtx.waitUntil(
      trackEvent('url_clicked', {
        shortCode,
        originalUrl: urlData.originalUrl,
        country: c.req.header('cf-ipcountry'),
        referer: c.req.header('referer'),
        userAgent: c.req.header('user-agent')
      })
    );
    
    // Redirect
    return c.redirect(urlData.originalUrl, 302);
    
  } catch (error) {
    console.error('Error redirecting:', error);
    return c.json({ error: 'Redirect failed' }, 500);
  }
});

// Get URL stats
app.get('/api/stats/:code', async (c) => {
  const shortCode = c.req.param('code');
  
  try {
    const urlData = await getUrlByCode(shortCode);
    
    if (!urlData) {
      return c.json({ error: 'Short URL not found' }, 404);
    }
    
    const analytics = await getAnalytics(shortCode);
    
    return c.json({
      shortCode,
      originalUrl: urlData.originalUrl,
      clicks: urlData.clicks,
      createdAt: urlData.createdAt,
      expiresAt: urlData.expiresAt,
      analytics
    });
    
  } catch (error) {
    console.error('Error getting stats:', error);
    return c.json({ error: 'Failed to get stats' }, 500);
  }
});

export default app;

Data Layer กับ Cloudflare KV

// storage.ts
import type { ShortUrl } from './types';

declare const URL_STORE: KVNamespace;
declare const ANALYTICS_STORE: KVNamespace;

export async function saveUrl(shortUrl: ShortUrl): Promise<void> {
  const key = `url:${shortUrl.shortCode}`;
  await URL_STORE.put(key, JSON.stringify(shortUrl), {
    expirationTtl: shortUrl.expiresAt 
      ? Math.floor((new Date(shortUrl.expiresAt).getTime() - Date.now()) / 1000)
      : undefined
  });
}

export async function getUrlByCode(shortCode: string): Promise<ShortUrl | null> {
  const key = `url:${shortCode}`;
  const data = await URL_STORE.get(key);
  
  if (!data) return null;
  
  try {
    return JSON.parse(data) as ShortUrl;
  } catch {
    return null;
  }
}

export async function incrementClicks(shortCode: string): Promise<void> {
  const urlData = await getUrlByCode(shortCode);
  if (!urlData) return;
  
  urlData.clicks += 1;
  await saveUrl(urlData);
}

export async function trackEvent(
  eventType: string, 
  data: Record<string, any>
): Promise<void> {
  const timestamp = Date.now();
  const key = `analytics:${eventType}:${timestamp}:${Math.random()}`;
  
  await ANALYTICS_STORE.put(key, JSON.stringify({
    eventType,
    timestamp,
    ...data
  }), {
    expirationTtl: 30 * 24 * 60 * 60 // 30 days
  });
}

export async function getAnalytics(shortCode: string): Promise<any> {
  const prefix = 'analytics:url_clicked:';
  const list = await ANALYTICS_STORE.list({ prefix });
  
  const clicks = [];
  for (const key of list.keys) {
    const data = await ANALYTICS_STORE.get(key.name);
    if (data) {
      const event = JSON.parse(data);
      if (event.shortCode === shortCode) {
        clicks.push(event);
      }
    }
  }
  
  // Aggregate data
  const countries = clicks.reduce((acc, click) => {
    acc[click.country] = (acc[click.country] || 0) + 1;
    return acc;
  }, {} as Record<string, number>);
  
  const dailyClicks = clicks.reduce((acc, click) => {
    const date = new Date(click.timestamp).toISOString().split('T')[0];
    acc[date] = (acc[date] || 0) + 1;
    return acc;
  }, {} as Record<string, number>);
  
  return {
    totalClicks: clicks.length,
    countries,
    dailyClicks,
    topReferers: getTopReferers(clicks)
  };
}

function getTopReferers(clicks: any[]): Record<string, number> {
  return clicks
    .filter(click => click.referer)
    .reduce((acc, click) => {
      const domain = new URL(click.referer).hostname;
      acc[domain] = (acc[domain] || 0) + 1;
      return acc;
    }, {} as Record<string, number>);
}

Utilities และ Helpers

// utils.ts
const CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

export function generateShortCode(length: number = 6): string {
  let result = '';
  for (let i = 0; i < length; i++) {
    result += CHARACTERS.charAt(Math.floor(Math.random() * CHARACTERS.length));
  }
  return result;
}

export function isValidUrl(string: string): boolean {
  try {
    const url = new URL(string);
    return url.protocol === 'http:' || url.protocol === 'https:';
  } catch {
    return false;
  }
}

export function sanitizeUrl(url: string): string {
  // Remove dangerous protocols
  const dangerous = ['javascript:', 'data:', 'vbscript:', 'file:'];
  const lower = url.toLowerCase().trim();
  
  for (const protocol of dangerous) {
    if (lower.startsWith(protocol)) {
      throw new Error('Dangerous URL protocol detected');
    }
  }
  
  // Add https if no protocol
  if (!url.match(/^https?:\/\//)) {
    return `https://${url}`;
  }
  
  return url;
}

Advanced Features ที่เพิ่มเข้าไป

1. Rate Limiting

// rate-limit.ts
import type { Context } from 'hono';

interface RateLimit {
  requests: number;
  resetTime: number;
}

declare const RATE_LIMIT_STORE: KVNamespace;

export async function rateLimitMiddleware(
  requests: number = 100,
  windowMs: number = 60 * 1000 // 1 minute
) {
  return async (c: Context, next: Function) => {
    const ip = c.req.header('cf-connecting-ip') || 'unknown';
    const key = `ratelimit:${ip}`;
    
    const now = Date.now();
    const windowStart = now - windowMs;
    
    // Get current rate limit data
    const data = await RATE_LIMIT_STORE.get(key);
    let rateLimit: RateLimit;
    
    if (data) {
      rateLimit = JSON.parse(data);
      
      // Reset if window expired
      if (rateLimit.resetTime < windowStart) {
        rateLimit = { requests: 0, resetTime: now + windowMs };
      }
    } else {
      rateLimit = { requests: 0, resetTime: now + windowMs };
    }
    
    // Check if limit exceeded
    if (rateLimit.requests >= requests) {
      const resetIn = Math.ceil((rateLimit.resetTime - now) / 1000);
      
      return c.json({
        error: 'Rate limit exceeded',
        code: 'RATE_LIMIT_EXCEEDED',
        resetIn
      }, 429);
    }
    
    // Increment and save
    rateLimit.requests += 1;
    await RATE_LIMIT_STORE.put(key, JSON.stringify(rateLimit), {
      expirationTtl: Math.ceil(windowMs / 1000)
    });
    
    // Add headers
    c.res.headers.set('X-RateLimit-Limit', requests.toString());
    c.res.headers.set('X-RateLimit-Remaining', (requests - rateLimit.requests).toString());
    c.res.headers.set('X-RateLimit-Reset', rateLimit.resetTime.toString());
    
    await next();
  };
}

// Usage
app.use('/api/*', rateLimitMiddleware(100, 60 * 1000));

2. Caching Strategy

// cache.ts
import type { Context } from 'hono';

export function cache(ttl: number = 300) { // 5 minutes default
  return async (c: Context, next: Function) => {
    const cacheKey = `cache:${c.req.url}`;
    
    // Try to get from cache first
    const cached = await caches.default.match(c.req);
    if (cached) {
      console.log('Cache HIT:', cacheKey);
      return cached;
    }
    
    console.log('Cache MISS:', cacheKey);
    await next();
    
    // Cache the response
    if (c.res.status === 200) {
      const response = c.res.clone();
      response.headers.set('Cache-Control', `public, max-age=${ttl}`);
      response.headers.set('X-Cache', 'MISS');
      
      // Store in edge cache
      c.executionCtx.waitUntil(
        caches.default.put(c.req, response)
      );
    }
  };
}

// Usage  
app.get('/api/stats/:code', cache(60), async (c) => {
  // ... stats logic
});

3. Error Handling และ Monitoring

// error-handler.ts
import type { Context } from 'hono';

export function errorHandler() {
  return async (c: Context, next: Function) => {
    try {
      await next();
    } catch (error) {
      console.error('Unhandled error:', error);
      
      // Log to external service (async)
      c.executionCtx.waitUntil(
        logError(error, {
          url: c.req.url,
          method: c.req.method,
          headers: Object.fromEntries(c.req.headers.entries()),
          userAgent: c.req.header('user-agent'),
          ip: c.req.header('cf-connecting-ip'),
          country: c.req.header('cf-ipcountry'),
          timestamp: new Date().toISOString()
        })
      );
      
      // Return user-friendly error
      if (error instanceof z.ZodError) {
        return c.json({
          error: 'Validation failed',
          code: 'VALIDATION_ERROR',
          details: error.errors
        }, 400);
      }
      
      return c.json({
        error: 'Internal server error',
        code: 'INTERNAL_ERROR'
      }, 500);
    }
  };
}

async function logError(error: any, context: any): Promise<void> {
  // Send to external logging service
  try {
    await fetch('https://api.logtail.com/logs', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer YOUR_TOKEN',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        level: 'error',
        message: error.message,
        error: {
          name: error.name,
          stack: error.stack
        },
        context
      })
    });
  } catch (logError) {
    console.error('Failed to log error:', logError);
  }
}

Performance Optimizations

1. Connection Reuse

// http-client.ts
class HTTPClient {
  private static instance: HTTPClient;
  private connections: Map<string, any> = new Map();
  
  static getInstance(): HTTPClient {
    if (!HTTPClient.instance) {
      HTTPClient.instance = new HTTPClient();
    }
    return HTTPClient.instance;
  }
  
  async fetch(url: string, options?: RequestInit): Promise<Response> {
    // Reuse connections for the same origin
    const origin = new URL(url).origin;
    
    return fetch(url, {
      ...options,
      // Cloudflare Workers handles connection pooling
      cf: {
        cacheTtl: 300,
        cacheEverything: false
      }
    });
  }
}

export const httpClient = HTTPClient.getInstance();

2. Smart Caching

// smart-cache.ts
export function smartCache(c: Context, key: string, ttl: number) {
  return {
    async get<T>(): Promise<T | null> {
      try {
        const cached = await caches.default.match(
          new Request(`https://cache.internal/${key}`)
        );
        
        if (cached) {
          const data = await cached.json();
          return data as T;
        }
        
        return null;
      } catch {
        return null;
      }
    },
    
    async set<T>(data: T): Promise<void> {
      try {
        const response = new Response(JSON.stringify(data), {
          headers: {
            'Content-Type': 'application/json',
            'Cache-Control': `public, max-age=${ttl}`,
            'X-Cache-Key': key
          }
        });
        
        c.executionCtx.waitUntil(
          caches.default.put(
            new Request(`https://cache.internal/${key}`),
            response
          )
        );
      } catch (error) {
        console.warn('Failed to cache data:', error);
      }
    }
  };
}

// Usage
app.get('/api/popular-urls', async (c) => {
  const cache = smartCache(c, 'popular-urls', 300);
  
  let popularUrls = await cache.get<ShortUrl[]>();
  if (!popularUrls) {
    popularUrls = await getPopularUrls();
    await cache.set(popularUrls);
  }
  
  return c.json(popularUrls);
});

Deployment และ Configuration

1. wrangler.toml

name = "url-shortener"
main = "src/index.ts"
compatibility_date = "2023-12-01"

# KV Namespaces
kv_namespaces = [
  { binding = "URL_STORE", id = "your-kv-namespace-id" },
  { binding = "ANALYTICS_STORE", id = "your-analytics-namespace-id" },
  { binding = "RATE_LIMIT_STORE", id = "your-ratelimit-namespace-id" }
]

# Environment Variables
[env.production.vars]
ALLOWED_ORIGINS = "https://myapp.com,https://admin.myapp.com"
LOG_LEVEL = "info"

[env.staging.vars] 
ALLOWED_ORIGINS = "https://staging.myapp.com"
LOG_LEVEL = "debug"

# Custom domains
[[env.production.routes]]
pattern = "short.ly/*"
zone_name = "short.ly"

[[env.staging.routes]]
pattern = "staging-short.ly/*"
zone_name = "short.ly"

2. GitHub Actions CI/CD

# .github/workflows/deploy.yml
name: Deploy to Cloudflare Workers

on:
  push:
    branches: [main, staging]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests
        run: npm test
        
      - name: Type check
        run: npm run type-check
        
      - name: Deploy to staging
        if: github.ref == 'refs/heads/staging'
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          environment: 'staging'
          
      - name: Deploy to production
        if: github.ref == 'refs/heads/main'
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          environment: 'production'

Testing Strategies

1. Unit Tests

// tests/utils.test.ts
import { describe, test, expect } from 'vitest';
import { generateShortCode, isValidUrl, sanitizeUrl } from '../src/utils';

describe('Utils', () => {
  describe('generateShortCode', () => {
    test('should generate code of correct length', () => {
      const code = generateShortCode(8);
      expect(code).toHaveLength(8);
    });
    
    test('should generate unique codes', () => {
      const codes = new Set();
      for (let i = 0; i < 1000; i++) {
        codes.add(generateShortCode());
      }
      expect(codes.size).toBe(1000);
    });
  });
  
  describe('isValidUrl', () => {
    test('should validate correct URLs', () => {
      expect(isValidUrl('https://example.com')).toBe(true);
      expect(isValidUrl('http://localhost:3000')).toBe(true);
    });
    
    test('should reject invalid URLs', () => {
      expect(isValidUrl('not-a-url')).toBe(false);
      expect(isValidUrl('javascript:alert(1)')).toBe(false);
    });
  });
});

2. Integration Tests

// tests/api.test.ts
import { describe, test, expect, beforeAll } from 'vitest';

// Mock Cloudflare Workers environment
const mockEnv = {
  URL_STORE: {
    get: vi.fn(),
    put: vi.fn(),
    delete: vi.fn()
  },
  ANALYTICS_STORE: {
    get: vi.fn(),
    put: vi.fn(),
    list: vi.fn()
  }
};

describe('API Endpoints', () => {
  beforeAll(() => {
    global.URL_STORE = mockEnv.URL_STORE;
    global.ANALYTICS_STORE = mockEnv.ANALYTICS_STORE;
  });
  
  test('POST /api/shorten should create short URL', async () => {
    const app = (await import('../src/index')).default;
    
    const req = new Request('https://test.com/api/shorten', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        url: 'https://example.com/very/long/url'
      })
    });
    
    mockEnv.URL_STORE.get.mockResolvedValue(null); // No existing URL
    mockEnv.URL_STORE.put.mockResolvedValue(undefined);
    
    const response = await app.fetch(req);
    const data = await response.json();
    
    expect(response.status).toBe(201);
    expect(data.shortUrl).toMatch(/https:\/\/short\.ly\/[a-zA-Z0-9]+/);
    expect(data.originalUrl).toBe('https://example.com/very/long/url');
  });
});

Performance Results ที่ได้จริง

Before (Express.js on AWS):

Location: Singapore → US East (Virginia)
-------------------------
Cold start: ~2000ms
Warm request: ~800ms
Database query: ~50ms
Network latency: ~400ms each way
Total average: ~860ms
99th percentile: ~1200ms

After (Hono + Cloudflare Workers):

Location: Singapore → Singapore Edge
-----------------------------------
Cold start: &lt;1ms  
Request processing: ~15ms
KV storage: ~5ms
Network latency: ~20ms
Total average: ~40ms
99th percentile: ~80ms

Performance improvement: 95% faster! 🚀

Production Lessons Learned

1. KV Storage Limitations

// ❌ อย่าใช้ KV สำหรับ high-frequency writes
async function incrementCounter(key: string) {
  const current = await KV_STORE.get(key) || '0';
  const newValue = parseInt(current) + 1;
  await KV_STORE.put(key, newValue.toString()); // Race condition!
}

// ✅ ใช้ Durable Objects สำหรับ consistent state
export class Counter {
  constructor(state: DurableObjectState) {
    this.state = state;
  }
  
  async increment(key: string): Promise<number> {
    let count = await this.state.storage.get(key) || 0;
    count += 1;
    await this.state.storage.put(key, count);
    return count;
  }
}

2. Global State Management

// ❌ อย่าใช้ global variables
let globalCache: Map<string, any> = new Map(); // ไม่ work ใน Workers

// ✅ ใช้ execution context หรือ external storage
app.use('*', async (c, next) => {
  // ใช้ c.set() สำหรับ request-scoped data
  c.set('startTime', Date.now());
  await next();
  
  const duration = Date.now() - c.get('startTime');
  console.log(`Request duration: ${duration}ms`);
});

3. Error Recovery

// Graceful degradation
app.get('/:code', async (c) => {
  try {
    const urlData = await getUrlByCode(shortCode);
    
    if (urlData) {
      return c.redirect(urlData.originalUrl);
    }
    
  } catch (error) {
    // KV storage error - fallback to external API
    try {
      const fallbackUrl = await getFallbackUrl(shortCode);
      if (fallbackUrl) {
        return c.redirect(fallbackUrl);
      }
    } catch (fallbackError) {
      // Complete failure - show friendly error
    }
  }
  
  return c.html(notFoundPage, 404);
});

Cost Analysis

AWS (Previous Setup):

EC2 t3.medium: $30/month
RDS PostgreSQL: $25/month  
Load Balancer: $18/month
Total: ~$73/month

Cloudflare Workers (Current):

Workers: $5/month (1M requests included)
KV storage: $0.50/month  
Custom domain: $0 (included)
Total: ~$5.50/month

Cost saving: 92% 💰

เครื่องมือและ Best Practices

1. Development Tools

# Local development
npm install -g wrangler
wrangler dev --local    # Run locally
wrangler dev           # Run with remote resources

# Testing
npm install vitest @cloudflare/workers-types
npm run test
npm run test:watch

# Deployment
wrangler deploy --env staging
wrangler deploy --env production

2. Monitoring

// Custom analytics
app.use('*', async (c, next) => {
  const start = Date.now();
  
  await next();
  
  const duration = Date.now() - start;
  
  // Log metrics (async)
  c.executionCtx.waitUntil(
    logMetrics({
      method: c.req.method,
      path: c.req.path,
      status: c.res.status,
      duration,
      country: c.req.header('cf-ipcountry'),
      datacenter: c.req.header('cf-colo')
    })
  );
});

3. Security Headers

app.use('*', async (c, next) => {
  await next();
  
  // Security headers
  c.res.headers.set('X-Frame-Options', 'DENY');
  c.res.headers.set('X-Content-Type-Options', 'nosniff');
  c.res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  c.res.headers.set('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
});

สรุป: Edge Computing ที่เปลี่ยนทุกอย่าง

ก่อนใช้ Hono + Cloudflare Workers:

  • API latency สูง (800ms+)
  • Server costs แพง ($70+/month)
  • Scaling ยาก
  • Cold starts ช้า

หลังใช้ Hono + Cloudflare Workers:

  • API latency ต่ำมาก (40ms average)
  • Costs ถูกมาก ($5/month)
  • Auto-scaling global
  • Cold starts เกือบไม่มี

ข้อดีที่ได้จริง:

  • Performance เร็วขึ้น 95%
  • Cost ถูกลง 92%
  • Scalability แบบ global อัตโนมัติ
  • Developer Experience ดีมาก (TypeScript first)
  • Deployment ง่ายและเร็ว

ข้อจำกัดที่ต้องรู้:

  • CPU time จำกัด 50ms per request
  • Memory จำกัด 128MB
  • Storage eventual consistency (KV)
  • WebSocket ไม่รองรับ
  • File system ไม่มี

คำแนะนำ:

  1. เหมาะกับ API ที่ต้องการ low latency
  2. ไม่เหมาะกับ long-running processes
  3. ใช้ KV storage สำหรับ read-heavy workloads
  4. ใช้ Durable Objects สำหรับ stateful applications
  5. Plan for global ตั้งแต่แรก

Hono + Cloudflare Workers มันเหมือน เครื่องบินไอพ่น ของ web development

เร็ว ประหยัด และไปได้ทั่วโลก! ✈️🌍

ตอนนี้ไม่อยากกลับไปใช้ traditional servers อีกแล้ว เพราะ edge computing มันอนาคตจริงๆ! 🚀