Cloudflare Workers ile Basit API Gateway Oluşturma

Edge computing’in güzelliği şu: kodun kullanıcıya mümkün olduğunca yakın çalışıyor. Cloudflare’in 300’den fazla PoP (Point of Presence) noktasıyla, yazdığın bir fonksiyon dünya genelinde milisaniyeler içinde çalışmaya başlıyor. Bugün bu altyapıyı kullanarak gerçek anlamda işe yarar bir API Gateway kuracağız. Sadece “Hello World” değil, rate limiting, authentication, request routing ve logging içeren, production’a alınabilir bir sistem.

Cloudflare Workers Nedir, Ne Değildir?

Klasik sunucu mimarisinde bir API Gateway kuruyorsanız, büyük ihtimalle Kong, AWS API Gateway veya nginx arkasına bir şeyler yazıyorsunuzdur. Bunların hepsinin ciddi bir altyapı maliyeti var. Cloudflare Workers ise V8 isolate’ler üzerinde çalışan, cold start süresi neredeyse sıfır olan bir edge runtime.

Ne kazanıyorsun:

  • Global dağıtım otomatik, ek konfigürasyon yok
  • Cold start yok (V8 isolate’ler millisecond altında başlıyor)
  • Ücretsiz planda günde 100.000 istek
  • KV storage, Durable Objects, R2 gibi entegre servisler

Neye dikkat etmeli:

  • CPU süresi sınırlı (ücretsiz planda 10ms, ücretli planda 50ms)
  • Node.js modüllerinin hepsi çalışmıyor, Web API’ları kullanıyorsun
  • Uzun süren işlemler için uygun değil

Geliştirme Ortamını Kurma

Wrangler CLI olmadan Cloudflare Workers ile iş yapamazsın. Hadi kuralım:

# Node.js 16+ gerekli
npm install -g wrangler

# Cloudflare hesabına bağlan
wrangler login

# Yeni proje oluştur
wrangler init api-gateway
cd api-gateway

# Bağımlılıkları kur
npm install

Proje oluşturulduğunda sana birkaç soru soruyor. “Hello World” worker seçersen temel yapı geliyor. Ama biz wrangler.toml dosyasını baştan yapılandıracağız:

# wrangler.toml
cat > wrangler.toml << 'EOF'
name = "api-gateway"
main = "src/index.js"
compatibility_date = "2024-01-01"

[vars]
ENVIRONMENT = "production"
API_VERSION = "v1"

[[kv_namespaces]]
binding = "RATE_LIMIT_KV"
id = "buraya_kv_id_gelecek"
preview_id = "buraya_preview_kv_id_gelecek"
EOF

KV namespace oluşturmak için:

# Production KV oluştur
wrangler kv:namespace create "RATE_LIMIT_KV"

# Preview (development) KV oluştur
wrangler kv:namespace create "RATE_LIMIT_KV" --preview

# Çıktıdaki ID'leri wrangler.toml'a ekle

Ana Gateway Yapısı

Şimdi işin özüne gelelim. src/index.js dosyamızı oluşturalım:

// src/index.js
import { handleAuth } from './middleware/auth.js';
import { handleRateLimit } from './middleware/rateLimit.js';
import { router } from './router.js';
import { corsHeaders } from './utils/cors.js';

export default {
  async fetch(request, env, ctx) {
    // OPTIONS preflight isteği için CORS yanıtı
    if (request.method === 'OPTIONS') {
      return new Response(null, {
        headers: corsHeaders,
      });
    }

    try {
      // 1. Rate limiting kontrolü
      const rateLimitResult = await handleRateLimit(request, env);
      if (!rateLimitResult.allowed) {
        return new Response(JSON.stringify({
          error: 'Too Many Requests',
          retryAfter: rateLimitResult.retryAfter,
        }), {
          status: 429,
          headers: {
            'Content-Type': 'application/json',
            'Retry-After': rateLimitResult.retryAfter.toString(),
            ...corsHeaders,
          },
        });
      }

      // 2. Authentication kontrolü
      const authResult = await handleAuth(request, env);
      if (!authResult.authenticated) {
        return new Response(JSON.stringify({
          error: 'Unauthorized',
          message: authResult.message,
        }), {
          status: 401,
          headers: {
            'Content-Type': 'application/json',
            ...corsHeaders,
          },
        });
      }

      // 3. İsteği route'a yönlendir
      const response = await router(request, env, authResult.user);

      // CORS header'larını ekle
      const newResponse = new Response(response.body, response);
      Object.entries(corsHeaders).forEach(([key, value]) => {
        newResponse.headers.set(key, value);
      });

      return newResponse;

    } catch (error) {
      console.error('Gateway error:', error);
      return new Response(JSON.stringify({
        error: 'Internal Server Error',
        requestId: crypto.randomUUID(),
      }), {
        status: 500,
        headers: {
          'Content-Type': 'application/json',
          ...corsHeaders,
        },
      });
    }
  },
};

Rate Limiting Middleware

Rate limiting, bir API Gateway’in olmazsa olmazı. KV storage kullanarak sliding window algoritması uygulayacağız:

// src/middleware/rateLimit.js
export async function handleRateLimit(request, env) {
  const clientIP = request.headers.get('CF-Connecting-IP') || 'unknown';
  const now = Date.now();
  const windowSize = 60 * 1000; // 1 dakika
  const maxRequests = 100; // Dakikada 100 istek

  const key = `rate_limit:${clientIP}:${Math.floor(now / windowSize)}`;

  // Mevcut sayacı al
  let currentCount = await env.RATE_LIMIT_KV.get(key, { type: 'json' });

  if (!currentCount) {
    currentCount = { count: 0, firstRequest: now };
  }

  currentCount.count += 1;

  // KV'ye geri yaz, TTL ile birlikte
  await env.RATE_LIMIT_KV.put(key, JSON.stringify(currentCount), {
    expirationTtl: Math.ceil(windowSize / 1000) + 10,
  });

  if (currentCount.count > maxRequests) {
    const windowEnd = Math.ceil(now / windowSize) * windowSize;
    const retryAfter = Math.ceil((windowEnd - now) / 1000);

    return {
      allowed: false,
      retryAfter,
      remaining: 0,
    };
  }

  return {
    allowed: true,
    remaining: maxRequests - currentCount.count,
    retryAfter: 0,
  };
}

JWT Authentication Middleware

API Gateway’imizde JWT tabanlı kimlik doğrulama kullanalım. Cloudflare Workers’da Node.js jsonwebtoken kütüphanesi çalışmıyor, Web Crypto API kullanmamız gerekiyor:

// src/middleware/auth.js

// Public endpoint'ler için authentication atlama
const PUBLIC_PATHS = [
  '/health',
  '/api/v1/auth/login',
  '/api/v1/auth/register',
];

export async function handleAuth(request, env) {
  const url = new URL(request.url);

  // Public endpoint kontrolü
  if (PUBLIC_PATHS.some(path => url.pathname.startsWith(path))) {
    return { authenticated: true, user: null };
  }

  const authHeader = request.headers.get('Authorization');

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return {
      authenticated: false,
      message: 'Authorization header eksik veya hatalı format',
    };
  }

  const token = authHeader.substring(7);

  try {
    const payload = await verifyJWT(token, env.JWT_SECRET);
    return {
      authenticated: true,
      user: payload,
    };
  } catch (error) {
    return {
      authenticated: false,
      message: 'Geçersiz veya süresi dolmuş token',
    };
  }
}

async function verifyJWT(token, secret) {
  const parts = token.split('.');
  if (parts.length !== 3) throw new Error('Invalid token format');

  const [headerB64, payloadB64, signatureB64] = parts;

  // Signature doğrulama
  const encoder = new TextEncoder();
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['verify']
  );

  const signature = base64UrlDecode(signatureB64);
  const data = encoder.encode(`${headerB64}.${payloadB64}`);

  const isValid = await crypto.subtle.verify('HMAC', key, signature, data);
  if (!isValid) throw new Error('Invalid signature');

  // Payload decode
  const payload = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));

  // Expiry kontrolü
  if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) {
    throw new Error('Token expired');
  }

  return payload;
}

function base64UrlDecode(str) {
  const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

Request Router ve Proxy

Şimdi gelen istekleri backend servislerimize yönlendirelim. Gerçek dünya senaryosunda birden fazla mikroservisimiz olduğunu varsayalım:

// src/router.js

const BACKEND_SERVICES = {
  users: 'https://users-service.internal.example.com',
  products: 'https://products-service.internal.example.com',
  orders: 'https://orders-service.internal.example.com',
};

export async function router(request, env, user) {
  const url = new URL(request.url);
  const pathname = url.pathname;

  // Route mapping
  if (pathname.startsWith('/api/v1/users')) {
    return proxyRequest(request, BACKEND_SERVICES.users, pathname, user);
  }

  if (pathname.startsWith('/api/v1/products')) {
    return proxyRequest(request, BACKEND_SERVICES.products, pathname, user);
  }

  if (pathname.startsWith('/api/v1/orders')) {
    // Order endpoint'leri için ek yetki kontrolü
    if (!user || user.role !== 'customer') {
      return new Response(JSON.stringify({ error: 'Forbidden' }), {
        status: 403,
        headers: { 'Content-Type': 'application/json' },
      });
    }
    return proxyRequest(request, BACKEND_SERVICES.orders, pathname, user);
  }

  if (pathname === '/health') {
    return new Response(JSON.stringify({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      region: request.cf?.colo || 'unknown',
    }), {
      headers: { 'Content-Type': 'application/json' },
    });
  }

  return new Response(JSON.stringify({ error: 'Route bulunamadı' }), {
    status: 404,
    headers: { 'Content-Type': 'application/json' },
  });
}

async function proxyRequest(request, backendUrl, pathname, user) {
  const targetUrl = `${backendUrl}${pathname}`;

  // Yeni header'lar ile isteği hazırla
  const headers = new Headers(request.headers);

  // Kullanıcı bilgilerini backend'e ilet
  if (user) {
    headers.set('X-User-Id', user.sub || '');
    headers.set('X-User-Role', user.role || '');
    headers.set('X-User-Email', user.email || '');
  }

  // Gateway'den geldiğini belirt
  headers.set('X-Gateway', 'cloudflare-workers');
  headers.set('X-Request-Id', crypto.randomUUID());
  headers.set('X-Forwarded-For', request.headers.get('CF-Connecting-IP') || '');

  // Orijinal Authorization header'ı kaldır, backend kendi auth'unu yapacak
  headers.delete('Authorization');

  const backendRequest = new Request(targetUrl, {
    method: request.method,
    headers,
    body: ['GET', 'HEAD'].includes(request.method) ? null : request.body,
  });

  try {
    const response = await fetch(backendRequest);
    return response;
  } catch (error) {
    return new Response(JSON.stringify({
      error: 'Backend service unavailable',
      service: backendUrl,
    }), {
      status: 503,
      headers: { 'Content-Type': 'application/json' },
    });
  }
}

CORS Utility ve Response Transformer

// src/utils/cors.js
export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Request-Id',
  'Access-Control-Max-Age': '86400',
};

// src/utils/responseTransformer.js
export function addRequestMetadata(response, request) {
  const newHeaders = new Headers(response.headers);

  newHeaders.set('X-Response-Time', Date.now().toString());
  newHeaders.set('X-Edge-Location', request.cf?.colo || 'unknown');
  newHeaders.set('X-Powered-By', 'Cloudflare Workers');

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: newHeaders,
  });
}

Secrets Yönetimi ve Deploy

API anahtarları ve JWT secret gibi hassas bilgileri environment variable olarak asla kaynak koduna yazmamalısın:

# Secret'ları Cloudflare'e ekle
wrangler secret put JWT_SECRET
# Prompt çıkacak, secret'ı gir

wrangler secret put API_KEY
# Prompt çıkacak, secret'ı gir

# Mevcut secret'ları listele
wrangler secret list

# Local development için .dev.vars dosyası oluştur
cat > .dev.vars << 'EOF'
JWT_SECRET=local-development-secret-minimum-32-chars
API_KEY=local-api-key-for-testing
ENVIRONMENT=development
EOF

# .gitignore'a ekle
echo ".dev.vars" >> .gitignore

# Local'de test et
wrangler dev

# Production'a deploy et
wrangler deploy

# Belirli bir environment'a deploy
wrangler deploy --env staging

Gerçek Dünya Senaryosu: E-ticaret API Gateway

Bir e-ticaret platformu için bu gateway’i nasıl kullandığımı anlatayım. Sistemde şu servisler vardı:

  • Kullanıcı servisi: Kayıt, giriş, profil yönetimi
  • Ürün servisi: Katalog, stok, fiyatlandırma
  • Sipariş servisi: Sepet, ödeme, kargo takibi
  • Bildirim servisi: E-posta, SMS, push notification

Gateway katmanında şunları çözdük:

Farklı servisler için farklı rate limit politikaları: Ürün arama endpoint’i dakikada 500 istek kabul ederken, sipariş oluşturma endpoint’i dakikada 10 ile sınırlandırıldı. Bu sayede bir kullanıcının sistemi spam’leyerek sipariş oluşturmasının önüne geçildi.

API versiyonlama: /api/v1/ ve /api/v2/ prefix’leri ile farklı backend versiyonlarına yönlendirme yapıldı. Yeni versiyona geçiş sırasında eski client’lar çalışmaya devam etti.

Geo-blocking: Cloudflare’in request.cf.country özelliği kullanılarak belirli ülkelerden gelen istekler engellendi veya farklı içerik sunuldu.

Canary deployment: Belirli kullanıcı segmentleri yeni backend versiyonuna yönlendirilirken, geri kalan kullanıcılar kararlı versiyonda kaldı.

İzleme ve Logging

Workers’da console.log kullandığında bu loglar Cloudflare Dashboard’da görünüyor. Ama daha gelişmiş bir çözüm için Logpush kullanabilirsin:

# Wrangler ile real-time log görme
wrangler tail

# Belirli bir filtre ile
wrangler tail --format pretty --status error

# JSON formatında
wrangler tail --format json | jq '.logs[].message'

# Logpush'u yapılandırmak için Cloudflare Dashboard'dan
# Account > Analytics > Logpush bölümüne git
# Workers Trace Events dataseti seç
# Hedef olarak R2, S3 veya third-party seç

Loglama için kodda şunu kullanıyorum:

// src/utils/logger.js
export function createLogger(request) {
  const requestId = crypto.randomUUID();
  const startTime = Date.now();

  return {
    requestId,
    info: (message, data = {}) => {
      console.log(JSON.stringify({
        level: 'INFO',
        message,
        requestId,
        timestamp: new Date().toISOString(),
        path: new URL(request.url).pathname,
        method: request.method,
        ip: request.headers.get('CF-Connecting-IP'),
        country: request.cf?.country,
        ...data,
      }));
    },
    error: (message, error = {}) => {
      console.error(JSON.stringify({
        level: 'ERROR',
        message,
        requestId,
        timestamp: new Date().toISOString(),
        error: error.message || error,
        stack: error.stack,
      }));
    },
    timing: () => Date.now() - startTime,
  };
}

Performans Optimizasyonları

Birkaç püf nokta:

  • Cache API kullanımı: GET isteklerini Workers Cache API ile cache’leyebilirsin. Ürün listesi gibi sık değişmeyen veriler için ciddi performans kazancı sağlıyor.
  • Streaming response: Büyük response’lar için TransformStream kullanarak veriyi stream et, tüm backend cevabını beklemeden client’a ilet.
  • Backend bağlantı havuzu: Her Worker instance ayrı bir V8 isolate içinde çalışır, bu yüzden geleneksel connection pooling mümkün değil. Bunun yerine Durable Objects kullanarak persistent bağlantı yönetimi yapılabilir.
  • KV okuma optimizasyonu: Rate limiting için her istek KV’yi okuyor ve yazıyor. Bunu azaltmak için ctx.waitUntil() ile async write yapabilirsin, kullanıcı cevabı beklemeden background’da yazar.

Sonuç

Cloudflare Workers ile kurduğumuz bu API Gateway, geleneksel çözümlere kıyasla çok daha az operasyonel yük getiriyor. Sunucu yönetimi yok, auto-scaling yok, cold start sorunu yok. Üstelik global dağıtım kutudan çıkıyor.

Ücretsiz plan günlük 100.000 istekle başlamak için oldukça yeterli. Paid plan ise 10 dolardan başlayıp aylık 10 milyon istek sunuyor. Kendi API Gateway altyapını kurmak için harcayacağın AWS veya GCP maliyetiyle kıyaslandığında ciddi bir tasarruf söz konusu.

Tabi her şey için uygun değil. CPU-intensive işlemler, uzun polling, WebSocket (artık destekliyor ama limitleri var) gibi senaryolarda dikkatli olmak gerekiyor. Ama HTTP tabanlı bir API Gateway için Cloudflare Workers şu an piyasadaki en pratik edge computing çözümlerinden biri. Bir dene, birkaç saat içinde production’a hazır bir gateway’in olacak.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir