JavaScript ile Fetch API Kullanımı: Modern Web İstekleri Rehberi

Modern web geliştirme dünyasında, arka uç servislerle konuşmak artık her developer’ın günlük işi haline geldi. Eskiden XMLHttpRequest ile başlayan bu macera, jQuery’nin $.ajax() metoduyla biraz daha insancıl bir hal aldı. Ancak gerçek anlamda temiz ve modern bir çözüm, Fetch API ile geldi. Sysadmin olarak ben de kendi monitoring araçlarımı, otomasyon scriptlerimi ve dashboard’larımı yazarken Fetch API’yi sıklıkla kullanıyorum. Bu yazıda Fetch API’yi gerçek dünya senaryolarıyla, sıfırdan ileri düzey kullanıma kadar ele alacağız.

Fetch API Nedir ve Neden Kullanmalısınız?

Fetch API, tarayıcı ortamında HTTP istekleri yapmak için kullanılan modern bir JavaScript arayüzüdür. Promise tabanlı yapısı sayesinde asenkron işlemleri çok daha temiz bir şekilde yönetebilirsiniz. Node.js 18+ sürümleriyle birlikte artık sunucu tarafında da native olarak kullanılabilir hale geldi, bu da onu hem frontend hem backend geliştirme için değerli bir araç yapıyor.

Neden Fetch API tercih edilmeli:

  • Promise tabanlı yapı: async/await ile çok daha okunabilir kod yazılır
  • Native destek: Ekstra kütüphane gerektirmez, tarayıcıya gömülü gelir
  • Esnek yapılandırma: Header, method, body gibi tüm HTTP parametrelerini kolayca ayarlayabilirsiniz
  • Stream desteği: Büyük veri akışlarını chunk chunk okuyabilirsiniz
  • Node.js 18+ desteği: Sunucu tarafında da aynı API’yi kullanabilirsiniz

Temel Kullanım: İlk GET İsteği

En basit haliyle Fetch API kullanımını görelim. Diyelim ki bir sunucunun sistem metriklerini döndüren bir API’niz var:

// Temel GET isteği
fetch('https://api.monitoring.local/metrics')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP hatası: ${response.status}`);
    }
    return response.json();
  })
  .then(data => {
    console.log('Metrikler alındı:', data);
  })
  .catch(error => {
    console.error('İstek başarısız:', error);
  });

Bu kod çalışır, ancak modern JavaScript’te async/await kullanmak çok daha temiz bir yapı sağlar:

async function getServerMetrics() {
  try {
    const response = await fetch('https://api.monitoring.local/metrics');
    
    if (!response.ok) {
      throw new Error(`Sunucu hatası: ${response.status} ${response.statusText}`);
    }
    
    const metrics = await response.json();
    return metrics;
  } catch (error) {
    console.error('Metrik alınamadı:', error.message);
    throw error;
  }
}

// Kullanım
const metrics = await getServerMetrics();
console.log(`CPU Kullanımı: ${metrics.cpu}%`);
console.log(`RAM Kullanımı: ${metrics.memory}%`);

Burada kritik bir nokta var: Fetch API, HTTP hata kodlarını (4xx, 5xx) otomatik olarak exception olarak fırlatmaz. Sadece network hatalarında (DNS çözümlenememe, bağlantı reddedilmesi vb.) promise reddedilir. Bu yüzden response.ok kontrolü yapmak şarttır.

POST İsteği ve Veri Gönderme

Gerçek dünyada çoğunlukla sadece veri okumakla kalmaz, veri de gönderirsiniz. Örneğin bir alerting sistemine yeni bir uyarı kuralı eklemek istiyorsunuz:

async function createAlertRule(ruleData) {
  const response = await fetch('https://api.monitoring.local/alerts/rules', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${process.env.API_TOKEN}`,
      'X-Request-ID': crypto.randomUUID()
    },
    body: JSON.stringify(ruleData)
  });

  if (!response.ok) {
    const errorBody = await response.json();
    throw new Error(`Kural oluşturulamadı: ${errorBody.message}`);
  }

  return await response.json();
}

// Kullanım örneği
const newRule = await createAlertRule({
  name: 'Yüksek CPU Uyarısı',
  condition: 'cpu_usage > 90',
  duration: '5m',
  severity: 'critical',
  notify: ['[email protected]']
});

console.log(`Kural oluşturuldu, ID: ${newRule.id}`);

Fetch API’nin init parametresi (ikinci argüman) oldukça kapsamlıdır:

  • method: GET, POST, PUT, DELETE, PATCH gibi HTTP metodları
  • headers: İstek başlıkları, Headers objesi veya düz obje olarak verilebilir
  • body: İstek gövdesi, string, FormData, Blob, ArrayBuffer veya ReadableStream olabilir
  • mode: cors, no-cors, same-origin değerlerini alır
  • credentials: include, same-origin, omit ile cookie yönetimi yapılır
  • cache: no-cache, reload, force-cache gibi önbellek stratejileri
  • signal: AbortController ile istek iptal etmek için kullanılır
  • redirect: follow, error, manual ile yönlendirme davranışı kontrol edilir

Gerçek Dünya Senaryosu: Sunucu Durumu Dashboard’u

Şimdi daha gerçekçi bir senaryo düşünelim. Bir monitoring dashboard’u yazıyorsunuz ve birden fazla sunucunun durumunu aynı anda sorgulamanız gerekiyor:

const servers = [
  { name: 'web-01', url: 'https://web-01.internal/health' },
  { name: 'web-02', url: 'https://web-02.internal/health' },
  { name: 'db-master', url: 'https://db-master.internal/health' },
  { name: 'cache-01', url: 'https://cache-01.internal/health' }
];

async function checkServerStatus(server) {
  const startTime = Date.now();
  
  try {
    const response = await fetch(server.url, {
      method: 'GET',
      signal: AbortSignal.timeout(5000) // 5 saniye timeout
    });
    
    const responseTime = Date.now() - startTime;
    const data = await response.json();
    
    return {
      name: server.name,
      status: response.ok ? 'healthy' : 'degraded',
      httpStatus: response.status,
      responseTime: `${responseTime}ms`,
      details: data
    };
  } catch (error) {
    return {
      name: server.name,
      status: 'down',
      error: error.message,
      responseTime: `${Date.now() - startTime}ms`
    };
  }
}

async function checkAllServers() {
  console.log('Sunucu durumları kontrol ediliyor...');
  
  // Promise.allSettled kullanıyoruz, bir sunucu down olsa bile diğerleri kontrol edilsin
  const results = await Promise.allSettled(
    servers.map(server => checkServerStatus(server))
  );
  
  results.forEach(result => {
    if (result.status === 'fulfilled') {
      const server = result.value;
      const emoji = server.status === 'healthy' ? '✓' : '✗';
      console.log(`${emoji} ${server.name}: ${server.status} (${server.responseTime})`);
    }
  });
}

// Her 30 saniyede bir kontrol et
await checkAllServers();
setInterval(checkAllServers, 30000);

Promise.allSettled() burada kritik öneme sahip. Promise.all() kullanırsanız, bir sunucu hata verdiğinde tüm istek grubu başarısız olur. allSettled() ise her istek için ayrı sonuç döndürür.

AbortController ile İstek İptal Etme

Uzun süren istekleri iptal etmek bazen kaçınılmaz olur. Örneğin kullanıcı sayfadan ayrılıyor veya yeni bir arama başlatıyor. AbortController bu durumlar için biçilmiş kaftandır:

class APIClient {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.activeRequests = new Map();
  }

  async request(endpoint, options = {}) {
    const requestId = crypto.randomUUID();
    const controller = new AbortController();
    
    this.activeRequests.set(requestId, controller);
    
    try {
      const response = await fetch(`${this.baseUrl}${endpoint}`, {
        ...options,
        signal: controller.signal
      });
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      
      return await response.json();
    } finally {
      this.activeRequests.delete(requestId);
    }
  }

  cancelAll() {
    this.activeRequests.forEach(controller => controller.abort());
    this.activeRequests.clear();
    console.log('Tüm aktif istekler iptal edildi');
  }
}

// Kullanım
const client = new APIClient('https://api.monitoring.local');

// Sayfa kapatılırken tüm istekleri iptal et
window.addEventListener('beforeunload', () => {
  client.cancelAll();
});

// Büyük log dosyası çekme isteği
try {
  const logs = await client.request('/logs/nginx?lines=10000');
  displayLogs(logs);
} catch (error) {
  if (error.name === 'AbortError') {
    console.log('İstek kullanıcı tarafından iptal edildi');
  } else {
    console.error('Log alınamadı:', error);
  }
}

Retry Mekanizması ile Güvenilir API İstekleri

Prodüksiyon ortamında geçici hatalar her zaman olur. Bir retry mekanizması yazmak, sistemin dayanıklılığını ciddi şekilde artırır:

async function fetchWithRetry(url, options = {}, retryConfig = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 10000,
    retryOn = [429, 500, 502, 503, 504]
  } = retryConfig;

  let lastError;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      // Başarılı istek veya retry edilmemesi gereken hata
      if (response.ok || !retryOn.includes(response.status)) {
        return response;
      }

      // Rate limiting durumunda Retry-After header'ına bak
      if (response.status === 429) {
        const retryAfter = response.headers.get('Retry-After');
        if (retryAfter) {
          const waitTime = parseInt(retryAfter) * 1000;
          console.log(`Rate limit aşıldı, ${waitTime}ms bekleniyor...`);
          await new Promise(resolve => setTimeout(resolve, waitTime));
          continue;
        }
      }

      lastError = new Error(`HTTP ${response.status}`);

    } catch (error) {
      lastError = error;
      
      // AbortError'da retry etme
      if (error.name === 'AbortError') {
        throw error;
      }
    }

    if (attempt < maxRetries) {
      // Exponential backoff + jitter
      const delay = Math.min(
        baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
        maxDelay
      );
      console.log(`Deneme ${attempt + 1} başarısız, ${Math.round(delay)}ms sonra tekrar denenecek...`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw new Error(`${maxRetries} denemeden sonra başarısız: ${lastError.message}`);
}

// Kullanım
try {
  const response = await fetchWithRetry(
    'https://api.external-service.com/data',
    {
      headers: { 'Authorization': `Bearer ${API_TOKEN}` }
    },
    {
      maxRetries: 4,
      baseDelay: 500,
      retryOn: [500, 502, 503, 504]
    }
  );
  
  const data = await response.json();
  console.log('Veri başarıyla alındı:', data);
} catch (error) {
  console.error('Tüm denemeler başarısız:', error.message);
  // Alerting sistemine bildir
  await sendAlert('critical', `API isteği başarısız: ${error.message}`);
}

Exponential backoff + jitter kombinasyonu burada önemli. Jitter olmadan tüm istemciler aynı anda retry yapar ve sunucuyu bunaltırsınız. Jitter ile dağıtık bir bekleme süresi oluşturursunuz.

FormData ile Dosya Yükleme

Log dosyalarını veya konfigürasyon dosyalarını bir API’ye yüklemek sık karşılaşılan bir senaryo:

async function uploadLogFile(file, serverName) {
  const formData = new FormData();
  formData.append('logfile', file);
  formData.append('server', serverName);
  formData.append('timestamp', new Date().toISOString());

  const response = await fetch('https://log-aggregator.internal/upload', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${LOG_API_TOKEN}`
      // Content-Type header'ı FormData ile kullanırken eklemeyin!
      // Tarayıcı bunu boundary bilgisiyle birlikte otomatik ayarlar
    },
    body: formData
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Log yüklenemedi: ${error}`);
  }

  const result = await response.json();
  console.log(`Log yüklendi: ${result.fileId}, boyut: ${result.size} bytes`);
  return result;
}

// Dosya input ile kullanım
document.getElementById('logFileInput').addEventListener('change', async (event) => {
  const file = event.target.files[0];
  if (!file) return;

  try {
    const result = await uploadLogFile(file, 'web-01');
    showNotification(`Dosya başarıyla yüklendi: ${result.fileId}`);
  } catch (error) {
    showNotification(`Hata: ${error.message}`, 'error');
  }
});

Önemli nokta: FormData kullanırken Content-Type header’ını manuel olarak ayarlamayın. Tarayıcı, multipart/form-data ile birlikte gerekli boundary değerini otomatik ekler. Manuel ayarlarsanız boundary eksik kalır ve sunucu isteği parse edemez.

Response Türlerini Doğru İşleme

Fetch API farklı response türlerini farklı metodlarla işler:

async function handleDifferentResponseTypes(url) {
  const response = await fetch(url);
  
  // Content-Type header'ına göre karar ver
  const contentType = response.headers.get('Content-Type') || '';
  
  if (contentType.includes('application/json')) {
    return await response.json();
    
  } else if (contentType.includes('text/')) {
    return await response.text();
    
  } else if (contentType.includes('application/octet-stream') || 
             contentType.includes('application/gzip')) {
    // Binary veri, örneğin compressed log dosyası
    const buffer = await response.arrayBuffer();
    return new Uint8Array(buffer);
    
  } else if (contentType.includes('image/')) {
    // Resim verisi
    return await response.blob();
    
  } else {
    // Bilinmeyen tip, text olarak al
    const text = await response.text();
    console.warn(`Bilinmeyen Content-Type: ${contentType}`);
    return text;
  }
}

// Büyük dosyaları stream olarak indirme
async function downloadLargeFile(url, filename) {
  const response = await fetch(url);
  
  if (!response.ok) {
    throw new Error(`İndirme başarısız: ${response.status}`);
  }

  const contentLength = response.headers.get('Content-Length');
  const total = parseInt(contentLength, 10);
  let loaded = 0;

  const reader = response.body.getReader();
  const chunks = [];

  while (true) {
    const { done, value } = await reader.read();
    
    if (done) break;
    
    chunks.push(value);
    loaded += value.length;
    
    if (total) {
      const progress = Math.round((loaded / total) * 100);
      console.log(`İndirme: %${progress} (${loaded}/${total} bytes)`);
    }
  }

  // Chunk'ları birleştir
  const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
  const result = new Uint8Array(totalLength);
  let offset = 0;
  for (const chunk of chunks) {
    result.set(chunk, offset);
    offset += chunk.length;
  }

  console.log(`${filename} indirildi: ${loaded} bytes`);
  return result;
}

Interceptor Pattern ile Merkezi API Yönetimi

Büyük projelerde her fetch çağrısına authentication, logging veya error handling eklemek yerine merkezi bir yapı kurmak hayat kurtarır:

class FetchInterceptor {
  constructor() {
    this.requestInterceptors = [];
    this.responseInterceptors = [];
  }

  addRequestInterceptor(fn) {
    this.requestInterceptors.push(fn);
    return this;
  }

  addResponseInterceptor(fn) {
    this.responseInterceptors.push(fn);
    return this;
  }

  async fetch(url, options = {}) {
    // Request interceptor'larını uygula
    let modifiedOptions = { ...options };
    for (const interceptor of this.requestInterceptors) {
      modifiedOptions = await interceptor(url, modifiedOptions);
    }

    let response = await fetch(url, modifiedOptions);

    // Response interceptor'larını uygula
    for (const interceptor of this.responseInterceptors) {
      response = await interceptor(response);
    }

    return response;
  }
}

// API client oluştur
const apiClient = new FetchInterceptor();

// Auth interceptor
apiClient.addRequestInterceptor(async (url, options) => {
  const token = localStorage.getItem('authToken') || await refreshToken();
  return {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${token}`,
      'X-Request-Time': Date.now().toString()
    }
  };
});

// Logging interceptor
apiClient.addRequestInterceptor(async (url, options) => {
  console.log(`[API] ${options.method || 'GET'} ${url}`);
  return options;
});

// 401 handler interceptor
apiClient.addResponseInterceptor(async (response) => {
  if (response.status === 401) {
    console.warn('Oturum süresi doldu, yönlendiriliyor...');
    window.location.href = '/login';
  }
  return response;
});

// Kullanım - artık tüm interceptor'lar otomatik çalışır
const response = await apiClient.fetch('https://api.monitoring.local/servers');
const servers = await response.json();

Güvenlik: CORS ve Credentials

API entegrasyonlarında güvenlik konuları kritiktir:

  • credentials: ‘include’: Cookie’leri cross-origin isteklere ekler, dikkatli kullanın
  • credentials: ‘same-origin’: Sadece aynı domain’e cookie gönderir, varsayılan değerdir
  • credentials: ‘omit’: Hiç cookie göndermez, public API’ler için idealdir
  • mode: ‘cors’: CORS header’larını zorunlu kılar, varsayılan değerdir
  • mode: ‘no-cors’: CORS hatalarını bastırır ama response body’e erişimi engeller
  • mode: ‘same-origin’: Cross-origin istekleri tamamen engeller
// Güvenli API isteği örneği
async function secureAPICall(endpoint, data) {
  const response = await fetch(`${API_BASE_URL}${endpoint}`, {
    method: 'POST',
    credentials: 'same-origin', // Cookie'leri sadece same-origin'e gönder
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
      'X-Requested-With': 'XMLHttpRequest'
    },
    body: JSON.stringify(data)
  });

  if (response.status === 403) {
    throw new Error('CSRF token geçersiz veya yetki yok');
  }

  return response;
}

Sonuç

Fetch API, modern JavaScript geliştirmesinin temel taşlarından biri haline geldi. Sysadmin perspektifinden bakıldığında, kendi monitoring araçlarınızı, otomasyon dashboard’larınızı veya internal tool’larınızı yazarken Fetch API size çok temiz ve yönetilebilir bir yapı sunar.

Bu yazıda ele aldığımız konuları özetlersek:

  • Temel GET/POST istekleri ve response.ok kontrolünün önemi
  • AbortController ile timeout ve istek iptali
  • Retry mekanizması ile exponential backoff ve jitter kombinasyonu
  • Promise.allSettled ile paralel isteklerde hata yönetimi
  • FormData ile dosya yükleme ve Content-Type uyarısı
  • Stream API ile büyük dosyaları progress takibiyle indirme
  • Interceptor pattern ile merkezi auth, logging ve error handling

En önemli nokta şu: Fetch API’yi doğru kullanmak, sadece fetch().then().catch() yazmaktan çok daha fazlası. Retry, timeout, hata yönetimi ve güvenlik konularını baştan düşünerek tasarlanmış bir API katmanı, prodüksiyon ortamında gece yarısı sizi uyandıracak olayların sayısını ciddi şekilde azaltır. Ve bir sysadmin olarak, gece yarısı uyumak paha biçilmez bir lükstür.

Bir yanıt yazın

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