İçeriğe geç
XDD.baby Yazılım Öğrenme
Sözlük Hata rehberi 0/56

Blok 4 · Ders 30

Dış API'lere bağlanmak

Sunucudan başka servislere istek atmak: API anahtarı, hız sınırı, zaman aşımı, hata yönetimi ve anahtarı saklayan ara sunucu.

Bu dersin sonunda

  • Bir API anahtarının neden tarayıcıdaki koda değil sunucuya ait olduğunu açıklayabilir, isteği kendi sunucundan geçiren bir ara sunucu kurabilirsin
  • Bir API belgesinde temel adresi, uç noktayı, parametreleri, kimlik doğrulamayı ve hız sınırını bulup isteği kurabilirsin
  • Node'da fetch ile .env'deki anahtarı başlıkta gönderebilir; 401, 429, 5xx ve JSON olmayan yanıtları tek bir fonksiyonda ayırt edebilirsin
  • AbortSignal.timeout ile isteğe süre sınırı koyabilir, geçici hatalarda artan beklemeyle yeniden deneyebilirsin
  • 429 ve Retry-After'a uyabilir, yanıtları süreli bir önbellekte tutarak dış servise giden istekleri azaltabilirsin
  • Riot Games API'sinde anahtar türlerini, hız sınırlarını ve bölgesel adresleri tanıyabilirsin
Bu sayfada
  1. Anahtar tarayıcıya girmez
  2. API belgesi okumak
  3. Sunucuda fetch
  4. Riot Games API'si: anahtar, sınırlar, bölgeler
  5. Anahtarı başlıkta göndermek
  6. Zaman aşımı: sonsuza dek bekleme
  7. Yeniden denemek ve artan bekleme
  8. Hız sınırı: 429 ve Retry-After
  9. Önbellek: aynı soruyu iki kez sorma
  10. Ara sunucu: hepsi bir arada
  11. Görevler
  12. Kendini sına
  13. Kayıt noktası

Ders 15'te Node'dan, Ders 24'te tarayıcıdan Data Dragon'a istek attın. Data Dragon kim olduğunu sormaz, sınır koymaz, çoğu zaman hızlı cevap verir. Gerçek servislerin çoğu öyle değildir: anahtar ister, ne sıklıkla istek atabileceğini sınırlar, bazen geç cevap verir, bazen hiç vermez. Bu derste kendi sunucunu dış servislerle konuşturuyorsun. Anahtarı gizli tutmayı, API belgesi okumayı, cevap gelmezse vazgeçmeyi, geçici hatalarda yeniden denemeyi, 429'a uymayı ve aynı soruyu tekrar tekrar sormamayı öğreneceksin. Dersin sonunda tarayıcının yalnız senin API'nle konuştuğu, anahtarın sunucuda kaldığı bir ara sunucun olacak.

Anahtar tarayıcıya girmez

Ders 25'teki ansiklopedine bir özellik eklemek istediğini düşün: ziyaretçi Riot ID'sini yazsın, sayfa onun hesabını göstersin. Bu bilgi Data Dragon'da yok, Riot'un asıl API'sinde. O API her istekte bir API anahtarı ister: Riot'un sana verdiği, seni tanıtan gizli bir metin. Ders 24'te "anahtarlı API'leri kendi sunucundan çağırmayı Blok 4'te öğreneceksin" demiştik; nedenini şimdi göreceksin. İlk akla gelen, js klasöründe hesap.js adında bir dosya açıp anahtarı oraya yazmak ve fetch'in başlığına koymak:

js/hesap.js✗ Yanlış
const anahtar = "RGAPI-12345678-abcd-4ef0-9876-0123456789ab" const yanit = await fetch("https://europe.api.riotgames.com/riot/account/v1/accounts/by-riot-id/Sihirdar/TR1", {    headers: { "X-Riot-Token": anahtar }})

Bu kod çalışır bile: denediğimizde Riot'un sunucusu tarayıcıdan gelen bu isteğe CORS izni verdi. Sorun başka yerde. js/hesap.js sayfanı açan herkesin tarayıcısına iner ve tarayıcıya giden hiçbir şey gizli kalmaz. Ziyaretçi F12'ye basıp Network sekmesinde isteğe tıklasa X-Riot-Token başlığını olduğu gibi görür (Ders 24). Tarayıcıya bile gerek yok; sayfa Live Server'da açıkken dosyayı terminalden indirmek yeter:

Terminal — PowerShell
PS C:\kod\ansiklopedi> curl.exe http://localhost:5500/js/hesap.jsconst anahtar = "RGAPI-12345678-abcd-4ef0-9876-0123456789ab" const yanit = await fetch("https://europe.api.riotgames.com/riot/account/v1/accounts/by-riot-id/Sihirdar/TR1", {    headers: { "X-Riot-Token": anahtar }})

Sayfayı GitHub Pages'e koyduysan (Ders 25) aynı dosya https://kullanici-adin.github.io/ansiklopedi/js/hesap.js adresinde bütün internete açık durur. .env ve .gitignore burada seni kurtarmaz: anahtar Git'e girmese bile tarayıcıya giden dosyanın içindedir.

Anahtarı ele geçiren biri senin adına istek atar: hız sınırını o doldurur, kurallara aykırı kullanılan anahtar kapatılır ve senin projen durur. Riot'un kuralları da açık: anahtarını güvende tutmak bir ürünü yayına açmanın şartı, güvende tutulmayan anahtar iptal edilebilir.

Çözüm, isteği tarayıcının değil senin sunucunun atması. Tarayıcı senin API'ne sorar; senin sunucun anahtarı .env'den alıp Riot'a sorar ve cevabı geri iletir. Araya giren bu sunucuya ara sunucu (proxy) denir.

Tarayıcı anahtar yok Senin sunucun anahtar .env'de Riot API anahtarı denetler 1 /api/hesap/Sihirdar/TR1 2 X-Riot-Token: RGAPI-… 3 hesabın bütün JSON'u 4 yalnız gereken alanlar ✗ doğrudan istek: anahtar sayfanın kodunda, herkes görür
Şekil 30.1 Tarayıcı yalnız senin sunucunla konuşur. Anahtarı bilen ve Riot'a soran sunucudur. Tarayıcıdan Riot'a doğrudan istek, anahtarı herkese vermek demektir.

Ara sunucunun faydası yalnız gizlilik değil:

  • Sınırları sen yönetirsin. Anahtarın hız sınırı bütün ziyaretçilerin için ortaktır. Aynı soruyu tekrar tekrar sormak yerine cevabı bir süre saklarsın; bu derste göreceksin.
  • Cevabı sen biçimlersin. Dış servisin kocaman JSON'undan sayfanın ihtiyacı olan birkaç alanı seçip gönderirsin.
  • CORS derdi biter. Ders 24'teki CORS bir tarayıcı kuralıydı; sunucudan sunucuya istekte yoktur.
  • Değişiklik tek yerde kalır. Dış servisin adresi ya da anahtarın değişirse yalnız sunucunu güncellersin, sayfa aynı kalır.

API belgesi okumak

Her API'nin kendi adresleri, parametre adları ve kuralları vardır; bunları tahminle bulamazsın. Hepsini API belgesi (documentation) söyler.

Belgeyi baştan sona okumana gerek yok. Beş sorunun cevabını arıyorsun:

SoruBelgede genelde nerede?
Temel adres ve uç nokta ne?"Base URL", "Endpoint", örnek istekler
Hangi parametreler var, hangileri zorunlu?"Parameters" tablosu
Anahtar gerekiyor mu, nereye konur?"Authentication", "API key"
Ne sıklıkla istek atabilirim?"Rate limits", "Terms", "Pricing"
Cevap ve hata neye benzer?"Response", "Errors"

İlk denemeyi Open-Meteo ile yapacağız: ücretsiz, anahtarsız bir hava durumu API'si. LoL'le ilgisi yok, ama anahtar istemediği için her çıktıyı kendi bilgisayarında hemen görebilirsin; Riot'un anahtarlı API'sine birazdan geçeceğiz. Belgeyi open-meteo.com/en/docs adresinde aç ve beş sorunun cevabını bul. Bu ders yazılırken bulduklarımız:

SoruOpen-Meteo'nun cevabı
AdresTemel adres https://api.open-meteo.com, uç nokta /v1/forecast
Parametrelerlatitude (enlem) ve longitude (boylam) zorunlu. current ile anlık değerleri istersin, ör. temperature_2m (sıcaklık), wind_speed_10m (rüzgâr hızı). timezone=auto saatleri o yerin saatine çevirir.
AnahtarTicari olmayan kullanımda yok
SınırGünde 10.000'den az, saatte 5.000, dakikada 600 istek
Cevap ve hataJSON; hatalı istekte 400 ve {"error":true,"reason":"..."}

Sınırlar ve ücretsiz kullanımın koşulları belgenin Terms sayfasında. Reklam ya da abonelikle para kazanan bir sitede ücretsiz sürümü kullanamazsın. Belgelerin sayfaları da sayıları da zamanla değişebilir; bir API'yi kullanmadan önce hep güncel belgeye bak.

Belgedeki örnek adresi tarayıcıya yapıştırabilir ya da terminalden isteyebilirsin. & işareti PowerShell'de özel bir anlam taşıdığı için adresi tırnak içine al:

Terminal — PowerShell
PS C:\kod> curl.exe "https://api.open-meteo.com/v1/forecast?latitude=41.01&longitude=28.98&current=temperature_2m,wind_speed_10m&timezone=auto"{"latitude":41.02444,"longitude":28.940546,"generationtime_ms":0.043392181396484375,"utc_offset_seconds":10800,"timezone":"Europe/Istanbul","timezone_abbreviation":"GMT+3","elevation":36.0,"current_units":{"time":"iso8601","interval":"seconds","temperature_2m":"°C","wind_speed_10m":"km/h"},"current":{"time":"2026-10-11T20:45","interval":900,"temperature_2m":21.1,"wind_speed_10m":1.6}}

Cevap tek uzun satır; terminali sağa kaydırarak okuyabilirsin. Aynı cevabı okunsun diye satırlara böldük:

Aynı cevap, satırlara bölünmüş
{  "latitude": 41.02444,  "longitude": 28.940546,  "generationtime_ms": 0.043392181396484375,  "utc_offset_seconds": 10800,  "timezone": "Europe/Istanbul",  "timezone_abbreviation": "GMT+3",  "elevation": 36.0,  "current_units": {    "time": "iso8601",    "interval": "seconds",    "temperature_2m": "°C",    "wind_speed_10m": "km/h"  },  "current": {    "time": "2026-10-11T20:45",    "interval": 900,    "temperature_2m": 21.1,    "wind_speed_10m": 1.6  }}

Cevabı belgeyle yan yana oku. current içinde istediğin iki değer var, birimleri current_units'te: °C ve km/h. interval: 900 bu değerlerin 900 saniyelik, yani 15 dakikalık dilimlerle hesaplandığını söylüyor; önbellek kurarken işine yarayacak. Koordinatların biraz değiştiğine de dikkat et: Open-Meteo hava modelinin en yakın ızgara noktasını kullanır. Sayılar sende farklı çıkacak; hava değişiyor.

Bir parametre adını yanlış yazarsan ne olur? temperature_2m yerine Türkçesini dene:

Terminal — PowerShell
PS C:\kod> curl.exe -i "https://api.open-meteo.com/v1/forecast?latitude=41.01&longitude=28.98&current=sicaklik"HTTP/1.1 400 Bad RequestDate: Sun, 11 Oct 2026 17:58:00 GMTContent-Type: application/json; charset=utf-8Content-Length: 218Connection: keep-alive {"error":true,"reason":"Invalid value: Cannot initialize SurfacePressureAndHeightVariable<VariableAndPreviousDay, VariableOrSpread<ForecastPressureVariable>, ForecastHeightVariable> from invalid String value sicaklik"}

400 Bad Request ve gövdede sebep (reason); error ile reason alanlarının sırası sende ters çıkabilir, JSON'da alan sırası önemsizdir. Mesaj uzun ve karışık, ama son sözcükleri yetiyor: invalid String value sicaklik, yani sicaklik geçersiz bir değer. Parametre adlarını belgeden harfi harfine kopyala; API'ler tahmini ya da Türkçe ad tanımaz.

Sunucuda fetch

Bu dersin projesi için yeni bir klasör aç: terminalde cd C:\kod, mkdir sihirdar-api ve cd sihirdar-api. Klasörü VS Code'da aç, Ders 27'deki gibi npm init -y ile package.json oluştur ve Ders 28'deki Express'i kur:

Terminal — PowerShell
PS C:\kod\sihirdar-api> npm install expressadded 65 packages, and audited 66 packages in 1s 28 packages are looking for funding  run `npm fund` for details found 0 vulnerabilities

Bu ders Express 5.3.0 ile denendi; seninki daha yeni olabilir. package.json'da "type" satırını "commonjs"'ten "module"'e çevir ve scripts kısmındaki test satırının yerine iki betik yaz. dev, Ders 29'daki gibi .env dosyasını okuyarak ve Ders 27'deki gibi her kayıtta yeniden başlayarak çalışır:

package.json
{  "name": "sihirdar-api",  "version": "1.0.0",  "description": "",  "main": "index.js",  "scripts": {    "dev": "node --watch --env-file=.env app.js",    "start": "node --env-file=.env app.js"  },  "keywords": [],  "author": "",  "license": "ISC",  "type": "module",  "dependencies": {    "express": "^5.3.0"  }}

Birazdan anahtar yazacağın .env dosyası Git'e asla girmemeli; işe .gitignore ile başla:

.gitignore
node_modules.env

İlk dış isteği bir sunucu kurmadan, düz bir dosyadan at. Proje klasöründe hava.js adında bir dosya aç ve belgede kurduğun adresi koda dök. Soldaki dosya listesinde artık node_modules, .gitignore, hava.js, package-lock.json ve package.json duruyor; aşağıdaki pencerede kod sığsın diye listeyi gizledik.

hava.js — sihirdar-api — Visual Studio Code
hava.js
const parametreler = new URLSearchParams({    latitude: "41.01",    longitude: "28.98",    current: "temperature_2m,wind_speed_10m",    timezone: "auto"})const adres = "https://api.open-meteo.com/v1/forecast?" + parametrelerconsole.log(adres) const yanit = await fetch(adres)const veri = await yanit.json()console.log(veri.current)const sicaklik = veri.current.temperature_2mconst ruzgar = veri.current.wind_speed_10mconsole.log(`İstanbul: ${sicaklik} °C, rüzgâr ${ruzgar} km/sa`)
PROBLEMSOUTPUTDEBUG CONSOLETERMINAL
PS C:\kod\sihirdar-api> node hava.jshttps://api.open-meteo.com/v1/forecast?latitude=41.01&longitude=28.98&current=temperature_2m%2Cwind_speed_10m&timezone=auto{  time: '2026-10-11T21:00',  interval: 900,  temperature_2m: 21,  wind_speed_10m: 1.6}İstanbul: 21 °C, rüzgâr 1.6 km/sa
Ln 1, Col 1Spaces: 4UTF-8JavaScript

Sayılar sende farklı çıkacak. İlk satır, URLSearchParams'ın kurduğu adres.

  • Ders 25'te adresteki sorguyu okumak için kullandığın URLSearchParams tersini de yapar: bir objeden sorgu metni kurar ve özel işaretleri kodlar. Çıktının ilk satırını sağa kaydırırsan current=temperature_2m%2Cwind_speed_10m kısmını görürsün: virgül %2C oldu. Metne eklenince kendiliğinden latitude=41.01&... metnine dönüşür. Adresi elle birleştirmek de çalışırdı, ama bir değerin içinde boşluk, & ya da Türkçe harf olunca adres bozulur; kodlama işini ona bırak.
  • fetch Node'da tarayıcıdakiyle aynıdır (Ders 15). Fark nerede çalıştığında: bu kodu ziyaretçi hiç görmez, CORS da ona karışmaz.
  • yanit.ok denetimini şimdilik atladık. Birazdan bütün denetimleri tek bir fonksiyonda toplayacağız.

Riot Games API'si: anahtar, sınırlar, bölgeler

Riot'un asıl API'si oyuncu hesaplarını, maç geçmişlerini ve lig sıralamalarını verir. Belgesi developer.riotgames.com adresinde. Data Dragon'dan iki farkı var: her istek bir anahtar ister ve her anahtarın bir hız sınırı vardır. Anahtarı şöyle alırsın:

  1. developer.riotgames.com adresine gir ve Login ile Riot hesabınla, yani oyuna girdiğin hesapla giriş yap.
  2. Giriş yapınca hesabına bağlı bir geliştirici anahtarı (development API key) oluşur ve portaldaki panelinde görünür. RGAPI- ile başlar; onu kopyala.
  3. Geliştirici anahtarı 24 saatte bir devre dışı kalır. Ertesi gün panelden yenile ve yeni anahtarı .env'e yaz.

Portalın görünümü değişmiş olabilir; adımlar aynı kalır: giriş yap, anahtarı kopyala, süresi dolunca yenile. Riot'ta üç tür anahtar var:

AnahtarNe için?Süre ve sınır
Geliştirici (development)Kendi bilgisayarında denemek, prototip yapmak24 saatte bir kapanır. Herkese açık bir ürün bununla yürütülemez.
Kişisel (personal)Kendin ya da küçük, kapalı bir topluluk içinÜrünün ayrıntılı bir açıklamasını ister, doğrulama süreci olmadan kaydedilir. 1 saniyede 20, 2 dakikada 100 istek; sınırı artırılmaz.
Üretim (production)Herkese açık site ve uygulamalarÇalışan bir prototip ister. Başlangıç sınırı 10 saniyede 500, 10 dakikada 30.000 istek.

Sayılar bu ders yazılırken Riot'un belgesinde yazanlar. Belge geliştirici anahtarı için ayrı bir sayı vermiyor; kendi anahtarının sınırına portalda bak. Sınırlar nasıl sayılır?

  • Bölge başına. Türkiye sunucusuna attığın istekler ile Batı Avrupa sunucusuna attıkların ayrı sayılır.
  • Uygulama sınırı ve metot sınırı. Uygulama sınırı, anahtarınla bir bölgede attığın bütün isteklerin toplamıdır. Metot sınırı ise her uç nokta için ayrıca sayılır.
  • Aşarsan 429. Yanıtın Retry-After başlığı kaç saniye bekleyeceğini söyler. Belgeye göre o süre boyunca hiç istek atmamalısın; kurala uymayan uygulamaların erişimi kapatılabilir.

Platform ve bölge adresleri

Riot'un tek bir adresi yok. Bazı veriler bir oyun sunucusuna aittir, bazıları bir bölgedeki bütün sunuculara ortaktır:

TürÖrnek temel adresHangi veriler?
Platform (sunucu)https://tr1.api.riotgames.comBir sunucuya ait veriler: sihirdar bilgisi, lig sıralaması
Bölgehttps://europe.api.riotgames.comHesaplar (account-v1), maç geçmişi

TR sunucusundaki bir oyuncu için platform tr1, bölge europe. Hesap uç noktasının (account-v1) belgesi yalnız üç bölge sayıyor: americas, asia ve europe. Belgeye göre herhangi bir hesabı herhangi bir bölgeden sorabilirsin, ama en yakın kümeyi kullanman öneriliyor; Türkiye'den en yakını europe. Bir uç noktanın platform mu bölge mi istediğini o uç noktanın belge sayfası söyler.

Oyundaki adın Sihirdar#TR1 gibi iki parçalıdır: oyun adı (gameName) ve etiket (tagLine). Hesap uç noktası bu ikisinden PUUID'yi bulur: hesabın hiç değişmeyen, 78 karakterlik kimliği. Maç geçmişi gibi öteki uç noktalar oyuncuyu bu kimlikle ister. Adreste # yok, çünkü # adreste parça (fragment) demektir ve sunucuya hiç gitmez (Ders 18). Bu yüzden API, oyun adını ve etiketi iki ayrı yol parçası olarak alır:

isteğin adresi https://europe.api.riotgames.com bölgesel temel adres europe, americas ya da asia /riot/account/v1/accounts/by-riot-id uç nokta (account-v1) Riot ID'den hesabı bulur /Sihirdar/TR1 yol parametreleri oyun adı ve etiket isteğin başlığı X-Riot-Token: RGAPI-… kimlik doğrulama her istekte, adreste değil
Şekil 30.2 Belgedeki bilgiler isteğin parçalarına dönüşür: bölgesel temel adres, uç nokta, yol parametreleri ve başlıktaki anahtar.

Belgede yanıt kodlarının listesi de var. Hangisinde ne yapacağın daha sonra yeniden deneme kurarken işine yarayacak:

KodBelgenin açıklamasıYeniden dene?
400İstek bozuk: eksik ya da hatalı parametreHayır, isteği düzelt
401Anahtar gönderilmediHayır
403Anahtar geçersiz ya da engellenmiş, ya da yol yanlışHayır
404Aranan kayıt yokHayır
415Content-Type başlığı yanlışHayır
429Hız sınırı aşıldıEvet, Retry-After kadar bekleyip
500Sunucuda beklenmeyen hataEvet
503Servis geçici olarak kullanılamıyorEvet

Belge ile gerçek her zaman birebir tutmaz. Bu ders yazılırken uydurma bir anahtarla denediğimizde Riot 403 değil, 401 döndürdü. Kodun ikisini de "anahtar sorunu" saymalı. Riot'un belgesi bir şey daha söylüyor: başarısız yanıtların gövdesi olmayabilir, olsa da JSON olmayabilir; kararını gövdeye değil durum koduna göre ver.

Anahtarı başlıkta göndermek

Anahtarını aldıysan proje klasöründe .env adında bir dosya aç ve tek satır yaz. Almadıysan aşağıdaki uydurma anahtarla devam et; bu bölümdeki hataların hepsini yine kendin görürsün.

.env
RIOT_API_KEY=RGAPI-12345678-abcd-4ef0-9876-0123456789ab

Sonra riot.js dosyasını yaz. Kendi Riot ID'n varsa ad ile etiket'e onu yaz:

riot.js
const ad = "Sihirdar"const etiket = "TR1"const adres = "https://europe.api.riotgames.com/riot/account/v1/accounts/by-riot-id/"    + encodeURIComponent(ad) + "/" + encodeURIComponent(etiket) const yanit = await fetch(adres, {    headers: { "X-Riot-Token": process.env.RIOT_API_KEY }})console.log(yanit.status)console.log(await yanit.json())
  • encodeURIComponent (Ders 25) oyun adındaki boşluğu ve Türkçe harfleri adrese uygun hâle getirir: Kırmızı Ejder → K%C4%B1rm%C4%B1z%C4%B1%20Ejder.
  • headers objesi isteğe başlık ekler. Ders 26'da GitHub'a anahtarı Authorization: Bearer … başlığında göndermiştin; Riot başka bir başlık istiyor, X-Riot-Token. Anahtarın hangi başlıkta gideceğini her zaman belge söyler.
  • Anahtar kodda değil, process.env'den geliyor.
  • 4. satır + ile başlıyor: JavaScript önceki satırı bitmemiş sayar ve ikisini tek ifade olarak okur. Uzun bir adresi böyle bölebilirsin.
Terminal — PowerShell
PS C:\kod\sihirdar-api> node --env-file=.env riot.js401{ status: { message: 'Unknown apikey', status_code: 401 } }

Uydurma anahtarla 401 ve Unknown apikey (tanınmayan anahtar) geldi. Geçerli bir anahtarla gelecek cevap aşağıdakine benzer. Bu çıktı bir örnek: bu ders geçerli bir anahtar olmadan yazıldı. Alan adları Riot'un belgesindeki gibi; puuid gerçekte harf ve rakamlardan oluşan 78 karakterlik bir metin.

Geçerli anahtarla (örnek)örnek, sende farklı çıkabilir
200{  puuid: 'aB3x…78 karakterlik kimlik…Qz9',  gameName: 'Sihirdar',  tagLine: 'TR1'}
Sık hata{ status: { message: 'Unknown apikey', status_code: 401 } }

Ne zaman çıkar? Riot anahtarını tanımayınca: anahtarı eksik kopyaladın, geliştirici anahtarının 24 saati doldu ya da yeniledikten sonra .env'i güncellemedin. Çıktısını hemen yukarıda gördün.

Nasıl düzelir? Portaldan anahtarı yeniden kopyala ve .env'deki satırın tamamını değiştir. .env yalnız program açılırken okunur: node --env-file=.env riot.js'i yeniden çalıştır. Birazdan yazacağın sunucu npm run dev ile açıksa ona dokunman gerekmez; --watch .env'i de izler ve dosyayı kaydedince sunucuyu kendiliğinden yeniden başlatır. Belge bu durum için 403 yazıyor, denediğimizde 401 geldi; ikisini de aynı şekilde ele al.

Sık hata{ status: { message: 'Forbidden', status_code: 401 } }: anahtar yerine undefined gitti

Ne zaman çıkar? --env-file=.env yazmadan çalıştırınca. .env okunmaz, process.env.RIOT_API_KEY undefined olur ve fetch başlığa undefined sözcüğünü yazar.

Terminal — PowerShell
PS C:\kod\sihirdar-api> node riot.js401{ status: { message: 'Forbidden', status_code: 401 } }

Riot, undefined gibi anahtara hiç benzemeyen bir metne Forbidden (yasak) diyor. Mesaj seni anahtarın yanlış olduğunu düşünmeye iter; oysa anahtar hiç gitmedi.

Nasıl düzelir? node --env-file=.env riot.js ile çalıştır. Emin değilsen console.log(process.env.RIOT_API_KEY) yaz: undefined görüyorsan .env okunmamıştır. Dosyanın adı tam olarak .env mi, proje klasöründe mi, --env-file yazdın mı, denetle. Sunucunda bu hatayı en baştan yakalamayı dersin sonunda göreceksin.

Sık hataCannot process request apikey or authorization header is empty

Ne zaman çıkar? Başlık hiç gitmeyince. En sık sebep headers yerine header yazmak: fetch tanımadığı ayarı hata vermeden yok sayar.

riot.js✗ Yanlış
const yanit = await fetch(adres, {    header: { "X-Riot-Token": process.env.RIOT_API_KEY }})
Terminal — PowerShell
PS C:\kod\sihirdar-api> node --env-file=.env riot.js401{  status: {    message: 'Cannot process request apikey or authorization header is empty',    status_code: 401  }}

Mesaj: "istek işlenemiyor, anahtar ya da yetki başlığı boş".

Nasıl düzelir? Ayarın adı çoğul: headers. Başlığın adını da belgeden kopyala: X-Riot-Token.

Her yanıtı denetleyen tek fonksiyon

riot.js, hava.js ve sunucundaki her uç nokta aynı denetimleri yapmalı: durum kodu başarılı mı, gelen şey gerçekten JSON mu? Ders 15'te ve Ders 24'te bunun için küçük bir jsonGetir yazmıştın: getir, yanit.ok'u denetle, JSON'a çevir. Şimdi daha güçlüsünü yazıp bir modüle koyacaksın (Ders 14); sunucundaki her dış istek onu kullanacak. disIstek.js adında bir dosya aç:

disIstek.js
export class DisServisHatasi extends Error {    constructor(durum, mesaj) {        super(mesaj)        this.name = "DisServisHatasi"        this.durum = durum    }} export async function jsonGetir(adres, basliklar = {}) {    const yanit = await fetch(adres, { headers: basliklar })    if (!yanit.ok) {        const govde = await yanit.text()        const ilkSatir = govde.split("\n")[0].slice(0, 120)        throw new DisServisHatasi(yanit.status, `${yanit.status} ${ilkSatir}`)    }    const tur = yanit.headers.get("content-type") ?? ""    if (!tur.includes("application/json")) {        throw new DisServisHatasi(yanit.status, `JSON yerine ${tur} geldi`)    }    return await yanit.json()}
  • DisServisHatasi, Ders 16'daki gibi kendi hata sınıfımız. Error'dan türediği için mesajı ve yığın izi var; üstüne durum alanında durum kodunu taşıyor. Hangi hatanın yeniden denenmeye değer olduğuna birazdan bu alana bakarak karar vereceğiz.
  • Yanıt başarısızsa gövdeyi text() ile düz metin olarak oku (Ders 26): gövde JSON olmayabilir, hiç olmayabilir de. Mesaja yalnız ilk satırı, en çok 120 harf koyuyoruz; bütün bir HTML sayfası terminali doldurmasın.
  • Yanıt başarılı olsa bile gelen şeyin JSON olduğunu Content-Type'tan denetle. Neden gerektiğini hemen aşağıdaki hata kartında göreceksin.

riot.js'i bu fonksiyonla yeniden yaz:

riot.js
import { jsonGetir } from "./disIstek.js" const ad = "Sihirdar"const etiket = "TR1"const adres = "https://europe.api.riotgames.com/riot/account/v1/accounts/by-riot-id/"    + encodeURIComponent(ad) + "/" + encodeURIComponent(etiket) try {    const hesap = await jsonGetir(adres, { "X-Riot-Token": process.env.RIOT_API_KEY })    console.log(hesap)} catch (hata) {    console.log(hata.name, hata.durum)    console.log(hata.message)}
Terminal — PowerShell
PS C:\kod\sihirdar-api> node --env-file=.env riot.jsDisServisHatasi 401401 {"status":{"message":"Unknown apikey","status_code":401}}

Program artık çökmüyor; ne olduğunu, hangi kodla olduğunu iki satırda söylüyor.

Sık hataSyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON

Ne zaman çıkar? API'nin adresi yerine belgenin sayfasını isteyince. Belgeden kopyalarken en sık yapılan yanlış bu: open-meteo.com/en/docs insanlar için bir web sayfası, api.open-meteo.com/v1/forecast ise programlar için API. Bu derste kısa denemeleri deneme.js adında bir dosyada yapacağız; onu aç ve şunu dene:

deneme.js
const yanit = await fetch("https://open-meteo.com/en/docs")console.log(yanit.status, yanit.ok)const veri = await yanit.json()console.log(veri.current)
Hata çıktısı
200 true<anonymous_script>:1<!doctype html>^ SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON    at JSON.parse (<anonymous>)    at parseJSONFromBytes (node:internal/deps/undici/undici:4394:19)    ... Node.js v24.20.0

Durum 200, ok da true: sunucu sayfayı memnuniyetle gönderdi. Yığın izinde senin dosyan bile yok; hata json()'ın içinde, gövde JSON'a çevrilirken çıktı. < ile başlayan gövde HTML'dir.

Nasıl düzelir? Adresi belgedeki örnek istekten kopyala. jsonGetir bu yüzden Content-Type'a da bakıyor; aynı adresi ona verirsen tek satırlık, anlaşılır bir mesaj alırsın:

deneme.js
import { jsonGetir } from "./disIstek.js" try {    const veri = await jsonGetir("https://open-meteo.com/en/docs")    console.log(veri)} catch (hata) {    console.log(hata.message)}
Terminal — PowerShell
PS C:\kod\sihirdar-api> node deneme.jsJSON yerine text/html; charset=utf-8 geldi

Adresteki api.'yi unutursan (https://open-meteo.com/v1/forecast?...) bu kez 404 ve yine bir HTML sayfası gelir; jsonGetir onu 404 <!doctype html> diye bildirir.

Zaman aşımı: sonsuza dek bekleme

Dış servis her zaman hızlı cevap vermez. Bazen yük altındadır, bazen ağın bir yerinde tıkanıklık vardır, bazen istek hiç cevaplanmaz; Ders 27'de res.end'i unuttuğunda olduğu gibi. fetch böyle bir servisi dakikalarca bekler. Senin sunucunda bunun anlamı şu: ziyaretçinin isteği de bekler, cevapsız istekler birikir, sayfa "yükleniyor"da takılı kalır.

Gerçek bir servisin ne zaman yavaşlayacağını sen seçemezsin. Bu yüzden onun huysuz hâllerini taklit eden küçük bir sahte servis yazacağız. Gerçek projelerde de böyle yapılır: kötü günü beklemek yerine kötü günü kendin üretirsin. Bu derste sahte servise üç yol ekleyeceksin: geç cevap veren, ara sıra hata veren ve sık sorunca 429 döndüren. İlki için proje klasöründe sahte-servis.js dosyasını aç:

sahte-servis.js
import express from "express" const app = express()const port = 4000 function bekle(ms) {    return new Promise((resolve) => setTimeout(resolve, ms))} app.use((req, res, next) => {    console.log(new Date().toLocaleTimeString("tr-TR"), req.method, req.url)    next()}) app.get("/yavas", async (req, res) => {    await bekle(3000)    res.json({ mesaj: "Geç de olsa geldim" })}) app.listen(port, (hata) => {    if (hata) {        throw hata    }    console.log(`Sahte servis çalışıyor: http://localhost:${port}`)})
  • bekle, Ders 15'teki fonksiyon. /yavas her isteği 3 saniye bekletip öyle cevaplar.
  • Ara katman (Ders 28) gelen her isteği saatiyle yazar: 21:12:41 GET /yavas gibi. toLocaleTimeString, Ders 17'deki toLocaleString'in yalnız saati yazan kardeşi. Servise ne zaman, kaç istek geldiğini buradan izleyeceğiz.
  • Port 4000: ara sunucun 3000'de çalışacak, ikisi çakışmasın. Bu derste iki sunucu birden çalıştıracağın için listen'daki hata denetimi (Ders 28) önemli: kapı doluysa sunucu sessizce "çalışıyor" demez, hatayla durur.

İkinci bir terminal aç (Ders 27) ve servisi --watch ile başlat; yeni yollar ekledikçe kendini yenilesin:

İkinci terminal: sahte servis
PS C:\kod\sihirdar-api> node --watch sahte-servis.jsSahte servis çalışıyor: http://localhost:4000

İlk terminalde yavas.js dosyasını yaz ve çalıştır. Süreyi Ders 15'teki console.time ile ölçüyoruz:

yavas.js
console.time("süre")const yanit = await fetch("http://localhost:4000/yavas")console.log(await yanit.json())console.timeEnd("süre")
Terminal — PowerShell
PS C:\kod\sihirdar-api> node yavas.js{ mesaj: 'Geç de olsa geldim' }süre: 3.114s

Şimdi isteğe bir süre sınırı koy. fetch'in ayar objesindeki signal (sinyal) alanı, isteği yarıda kesebilen bir kumanda alır. AbortSignal.timeout(2000) 2000 milisaniye sonra kendiliğinden basılan bir kumanda üretir:

yavas.js
console.time("süre")const yanit = await fetch("http://localhost:4000/yavas", {    signal: AbortSignal.timeout(2000)})console.log(await yanit.json())console.timeEnd("süre")
Sık hataDOMException [TimeoutError]: The operation was aborted due to timeout

Ne zaman çıkar? AbortSignal.timeout ile verdiğin süre dolunca istek kesilir ve fetch bu hatayı fırlatır. Yakalamazsan program çöker:

Terminal — PowerShell
PS C:\kod\sihirdar-api> node yavas.jsnode:internal/modules/run_main:107    triggerUncaughtException(    ^ [DOMException [TimeoutError]: The operation was aborted due to timeout] Node.js v24.20.0

Mesaj: "işlem zaman aşımı yüzünden iptal edildi". DOMException tarayıcı dünyasından gelen bir hata türü, Node da aynısını kullanıyor. Hatanın adı (name) TimeoutError.

Nasıl düzelir? Zaman aşımı bir kusur değil, bir karar: süre dolunca ne olacağını sen seçersin. try/catch ile yakala; hata.name === "TimeoutError" ise kullanıcıya anlaşılır bir şey söyle ya da yeniden dene.

yavas.js
console.time("süre")try {    const yanit = await fetch("http://localhost:4000/yavas", {        signal: AbortSignal.timeout(2000)    })    console.log(await yanit.json())} catch (hata) {    console.log(hata.name + ": " + hata.message)}console.timeEnd("süre")
Terminal — PowerShell
PS C:\kod\sihirdar-api> node yavas.jsTimeoutError: The operation was aborted due to timeoutsüre: 2.005s

Program tam 2 saniyede vazgeçti. Sahte servisin terminaline bak:

İkinci terminal: sahte servis
21:12:41 GET /yavas21:12:44 GET /yavas21:12:46 GET /yavas

Üç çalıştırma, üç istek. Servis, senin vazgeçtiğinden habersiz her isteği yine 3 saniyede bitirdi. Zaman aşımı yalnız senin beklemeni keser; karşı tarafın işini durdurmaz.

Kaç saniye vermelisin? Servisin normalde ne kadar sürdüğüne bak ve birkaç katını ver. Open-Meteo'ya attığımız istekler yarım saniyenin altında döndü; 2 saniye cömert bir sınır. Unutma: tarayıcıdaki ziyaretçi senin sunucunu, senin sunucun da dış servisi bekliyor. Ziyaretçiyi ne kadar bekletebileceğini düşün; dış servise tanıdığın süre ondan kısa olmalı.

Süre sınırını jsonGetir'e ekle; böylece her dış istek kendiliğinden korunur. fetch satırını şöyle değiştir:

disIstek.js
export async function jsonGetir(adres, basliklar = {}) {    const yanit = await fetch(adres, {        headers: basliklar,        signal: AbortSignal.timeout(2000)    })

Yeniden denemek ve artan bekleme

Bir istek başarısız oldu. Yeniden denemeli misin? Cevap, hatanın türüne bağlı. Ders 26'daki kural burada da geçerli: 4 ile başlayan kodlar "sorun sende" der, aynı isteği yeniden göndermek bir şey değiştirmez. 5 ile başlayanlar, zaman aşımı ve bağlantı kopması ise çoğu zaman geçicidir; az sonra aynı istek başarılı olabilir. Tek istisna 429: isteğin doğru, ama çok sık soruyorsun; beklersen geçer.

Ne geldi?Yeniden dene?Neden?
400, 404Hayırİstek yanlış; aynısı yine yanlış olur.
401, 403HayırAnahtar sorunu; beklemek düzeltmez.
429Evet, söylenen kadar bekleyipSınır doldu, zamanla açılır.
500, 502, 503, 504EvetServis geçici olarak sorunlu.
Zaman aşımı, fetch failedEvetAğ ya da servis o an yavaş veya ulaşılamıyor.

Hemen yeniden denemek de iyi fikir değil. Servis yük altında olduğu için hata veriyorsa, ona üst üste yeni istekler göndermek onu daha da boğar.

Sahte servise ara sıra hata veren bir yol ekle. /yavas'ın altına yaz:

sahte-servis.js
let istekSayisi = 0 app.get("/kararsiz", (req, res) => {    istekSayisi = istekSayisi + 1    if (istekSayisi % 3 !== 0) {        res.status(503).json({ hata: "Servis meşgul" })        return    }    res.json({ mesaj: "Bu sefer oldu", istek: istekSayisi })})

Her üç istekten ilk ikisi 503 Service Unavailable (servis kullanılamıyor) alır, üçüncüsü başarılı olur; % kalanı Ders 1'den tanıyorsun. Şimdi yeniden deneyen fonksiyonu disIstek.js'in sonuna ekle:

disIstek.js
function bekle(ms) {    return new Promise((resolve) => setTimeout(resolve, ms))} function geciciMi(hata) {    if (hata.name === "TimeoutError" || hata.message === "fetch failed") {        return true    }    return hata.durum === 429 || hata.durum >= 500} export async function yenidenDene(is, denemeSayisi = 3) {    let bekleme = 500    for (let deneme = 1; deneme <= denemeSayisi; deneme++) {        try {            return await is()        } catch (hata) {            if (deneme === denemeSayisi || !geciciMi(hata)) {                throw hata            }            console.log(`${deneme}. deneme olmadı (${hata.message}). ${bekleme} ms sonra yeniden.`)            await bekle(bekleme)            bekleme = bekleme * 2        }    }}
  • is, yeniden denenecek işi yapan bir fonksiyon. yenidenDene onu her denemede baştan çağırır; ona sonucu değil, sonucu üreten fonksiyonu veriyorsun (Ders 10).
  • return await is(): iş başarılı olursa sonucu döndür, döngü biter. Hata olursa catch'e düşülür.
  • Son denemeyse ya da hata geçici değilse hatayı olduğu gibi yukarı fırlat. Yakalayıp yutmuyoruz (Ders 13).
  • Yoksa bekle ve bir sonraki bekleme için süreyi ikiye katla.
  • geciciMi, yukarıdaki tablonun koda dökülmüş hâli. fetch failed, isteğin sunucuya hiç ulaşamadığı durumdur (Ders 15).

deneme.js'in içini şununla değiştir ve çalıştır:

deneme.js
import { jsonGetir, yenidenDene } from "./disIstek.js" console.time("toplam")const veri = await yenidenDene(() => jsonGetir("http://localhost:4000/kararsiz"))console.log(veri)console.timeEnd("toplam")
Terminal — PowerShell
PS C:\kod\sihirdar-api> node deneme.js1. deneme olmadı (503 {"hata":"Servis meşgul"}). 500 ms sonra yeniden.2. deneme olmadı (503 {"hata":"Servis meşgul"}). 1000 ms sonra yeniden.{ mesaj: 'Bu sefer oldu', istek: 3 }toplam: 1.641s
İkinci terminal: sahte servis
21:13:02 GET /kararsiz21:13:02 GET /kararsiz21:13:03 GET /kararsiz

Servis üç istek gördü: ikisini reddetti, üçüncüsünü cevapladı. Toplam süre, 500 ve 1000 milisaniyelik iki bekleme artı isteklerin kendi süreleri. İkinci kez çalıştırırsan istek: 6 görürsün; sayaç servis yeniden başlayana kadar kaldığı yerden sayar.

503 gelince: artan bekleme 1 ✗ 500 ms 2 ✗ 1000 ms 3 ✓ 503 503 200 beklemeler ikiye katlanarak uzar 429 gelince: Retry-After kadar 1 ✗ 3000 ms: servis "3 saniye bekle" dedi 2 ✓ 429, Retry-After: 3 200
Şekil 30.3 Yeniden deneme zaman çizelgesi. Üstte 503: her başarısız denemeden sonra bekleme ikiye katlanıyor. Altta 429: servisin Retry-After ile söylediği kadar bekleniyor. Çubukların uzunluğu bekleme süresiyle orantılı.

Şimdi yeniden denemeye değmeyen bir hata dene: deneme.js'teki adresi /kararsiz yerine olmayan bir yol yap, /yok:

Terminal — PowerShell
PS C:\kod\sihirdar-api> node deneme.jsfile:///C:/kod/sihirdar-api/disIstek.js:17        throw new DisServisHatasi(yanit.status, `${yanit.status} ${ilkSatir}`)              ^ DisServisHatasi: 404 <!DOCTYPE html>    at jsonGetir (file:///C:/kod/sihirdar-api/disIstek.js:17:15)    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)    at async yenidenDene (file:///C:/kod/sihirdar-api/disIstek.js:41:20)    at async file:///C:/kod/sihirdar-api/deneme.js:4:14 {  durum: 404} Node.js v24.20.0

Hiç "deneme olmadı" satırı yok: 404 geçici değil, yenidenDene ilk hatada vazgeçti. Express, olmayan bir yola kendi HTML sayfasıyla cevap verdiği için mesajda <!DOCTYPE html> var. Yığın izini aşağıdan yukarı oku (Ders 13): deneme.js:4 yenidenDene'yi çağırdı, o jsonGetir'i çağırdı, hata disIstek.js:17'deki throw'da fırladı. Hatanın sonundaki durum: 404, sınıfımıza eklediğimiz alan. Denedikten sonra adresi yeniden /kararsiz yap.

Sık hataYeniden deneme hiç çalışmıyor: return await yerine return

Ne zaman çıkar? try içinden bir Promise'i await yazmadan döndürünce. Fark tek bir sözcük:

disIstek.js✗ Yanlış
        try {            return is()        } catch (hata) {
Terminal — PowerShell
PS C:\kod\sihirdar-api> node deneme.jsfile:///C:/kod/sihirdar-api/disIstek.js:17        throw new DisServisHatasi(yanit.status, `${yanit.status} ${ilkSatir}`)              ^ DisServisHatasi: 503 {"hata":"Servis meşgul"}    at jsonGetir (file:///C:/kod/sihirdar-api/disIstek.js:17:15)    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)    at async file:///C:/kod/sihirdar-api/deneme.js:4:14 {  durum: 503} Node.js v24.20.0

Sahte servis yalnız bir istek gördü. return is() Promise'i beklemeden geri verir; Promise sonradan reddedildiğinde try çoktan bitmiştir ve catch hiç çalışmaz. Yığın izinde yenidenDene bile yok: hata çıktığında o fonksiyon çoktan dönmüştü.

Nasıl düzelir? return await is() yaz. try içinden bir Promise döndürürken await'i unutma.

Sık hataTypeError: fetch failed ve altında ECONNREFUSED

Ne zaman çıkar? İstek attığın adreste dinleyen bir program yokken: sahte servisi başlatmayı unuttun, Ctrl+C ile durdurdun ya da portu yanlış yazdın.

Terminal — PowerShell
PS C:\kod\sihirdar-api> node deneme.js1. deneme olmadı (fetch failed). 500 ms sonra yeniden.2. deneme olmadı (fetch failed). 1000 ms sonra yeniden.node:internal/modules/run_main:107    triggerUncaughtException(    ^ [TypeError: fetch failed] {  [cause]: AggregateError [ECONNREFUSED]:       ...    code: 'ECONNREFUSED',    [errors]: [      Error: connect ECONNREFUSED ::1:4000          ...        errno: -4078,        code: 'ECONNREFUSED',        syscall: 'connect',        address: '::1',        port: 4000      },      Error: connect ECONNREFUSED 127.0.0.1:4000          ...        errno: -4078,        code: 'ECONNREFUSED',        syscall: 'connect',        address: '127.0.0.1',        port: 4000      }    ]  }} Node.js v24.20.0

fetch failed geçici sayıldığı için iki kez daha denendi, sonra hata yukarı çıktı. Asıl sebep [cause]'da: ECONNREFUSED, "bağlantı reddedildi". Node localhost'u iki adresle denedi (::1 ve 127.0.0.1); ikisinde de kapının arkasında kimse yoktu. Ders 27'de curl.exe'nin Failed to connect dediği durumun Node'daki karşılığı bu.

İnternet bağlantın tamamen kopuksa da en üst satır aynı olur: TypeError: fetch failed. O zaman sebep satırı çoğu zaman alan adının bulunamadığını söyleyen bir getaddrinfo hatası olur; Ders 15'te yanlış yazılmış alan adında gördüğün ENOTFOUND gibi.

Nasıl düzelir? Servisin terminaline bak; çalışmıyorsa node --watch sahte-servis.js ile başlat. Adresteki portu denetle.

Hız sınırı: 429 ve Retry-After

Riot'un kişisel anahtarı bir saniyede 20, iki dakikada 100 istek atabilir. Bu senin değil, anahtarın sınırı: siteni aynı anda yüz kişi kullanıyorsa yüzünün isteği de aynı sınırdan düşer. Sınırı aşan istek 429 Too Many Requests (çok fazla istek) alır; Retry-After başlığı kaç saniye sonra yeniden deneyebileceğini söyler.

Sahte servise üç saniyede yalnız bir isteğe izin veren bir yol ekle:

sahte-servis.js
let sonIzin = 0 app.get("/sinirli", (req, res) => {    const gecen = Date.now() - sonIzin    if (gecen < 3000) {        const kalan = Math.ceil((3000 - gecen) / 1000)        res.set("Retry-After", String(kalan))        res.status(429).json({ hata: "Çok sık istek attın" })        return    }    sonIzin = Date.now()    res.json({ mesaj: "Buyur, taze veri" })})
  • Date.now() (Ders 23) şu anı milisaniye olarak verir. Son izinden bu yana 3000 ms geçmediyse 429.
  • res.set("Retry-After", ...) yanıta bir başlık ekler. Değer saniye cinsinden bir metin; Math.ceil yukarı yuvarlar, 2,4 saniye kaldıysa 3 yazar.
  • Kendi API'ne böyle bir sınır koymayı Ders 31'de göreceksin; buradaki sayaç yalnız deney için.

İlk terminalden arka arkaya iki istek at:

Terminal — PowerShell
PS C:\kod\sihirdar-api> curl.exe -i http://localhost:4000/sinirliHTTP/1.1 200 OKX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 28ETag: W/"1c-gl2dmevYKYHYri1iik8KeH1f/Uk"Date: Sun, 11 Oct 2026 18:13:29 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"mesaj":"Buyur, taze veri"}PS C:\kod\sihirdar-api> curl.exe -i http://localhost:4000/sinirliHTTP/1.1 429 Too Many RequestsX-Powered-By: ExpressRetry-After: 3Content-Type: application/json; charset=utf-8Content-Length: 33ETag: W/"21-eFit0VoCMedPBP/11ZdCOpSy2bI"Date: Sun, 11 Oct 2026 18:13:29 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"Çok sık istek attın"}

İkinci istek aynı saniyede gitti: 429 ve Retry-After: 3. X-Powered-By ve ETag başlıklarını Express kendiliğinden ekler. Şimdi aynı yolu programdan, yenidenDene ile üç kez arka arkaya iste. curl.exe denemesinden sonra üç saniye geçmesini bekle ve deneme.js'in içini şununla değiştir:

deneme.js
import { jsonGetir, yenidenDene } from "./disIstek.js" for (let i = 1; i <= 3; i++) {    console.time(`${i}. istek`)    const veri = await yenidenDene(() => jsonGetir("http://localhost:4000/sinirli"))    console.log(veri.mesaj)    console.timeEnd(`${i}. istek`)}
Sık hataDisServisHatasi: 429 {"hata":"Çok sık istek attın"}

Ne zaman çıkar? Servisin hız sınırını aşınca ve yeniden denerken Retry-After'ın söylediğinden kısa bekleyince. Şu anki yenidenDene tam olarak bunu yapıyor:

Terminal — PowerShell
PS C:\kod\sihirdar-api> node deneme.jsBuyur, taze veri1. istek: 68.77ms1. deneme olmadı (429 {"hata":"Çok sık istek attın"}). 500 ms sonra yeniden.2. deneme olmadı (429 {"hata":"Çok sık istek attın"}). 1000 ms sonra yeniden.file:///C:/kod/sihirdar-api/disIstek.js:17        throw new DisServisHatasi(yanit.status, `${yanit.status} ${ilkSatir}`)              ^ DisServisHatasi: 429 {"hata":"Çok sık istek attın"}    at jsonGetir (file:///C:/kod/sihirdar-api/disIstek.js:17:15)    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)    at async yenidenDene (file:///C:/kod/sihirdar-api/disIstek.js:41:20)    at async file:///C:/kod/sihirdar-api/deneme.js:5:18 {  durum: 429} Node.js v24.20.0

İlk istek geçti. İkincisi 429 aldı; yenidenDene onu geçici saydı ama Retry-After'a bakmadı. Servis "3 saniye bekle" demişti, kod 500 ms ve 1000 ms bekledi. Üç deneme de bekleme süresinin içinde kaldı, üçü de 429 aldı ve hata yukarı çıktı. Gerçek bir serviste bu, sınırı zorlamaya devam etmek demektir; Riot'un kurallarına göre erişimin kapatılabilir.

Nasıl düzelir? Servis "üç saniye bekle" dediyse üç saniye bekle. Aşağıda yenidenDene'yi Retry-After'a uyacak hâle getiriyoruz.

disIstek.js'te iki yeri değiştir. Önce hata sınıfı ve jsonGetir başlığı okusun:

disIstek.js
export class DisServisHatasi extends Error {    constructor(durum, mesaj, tekrarSaniye) {        super(mesaj)        this.name = "DisServisHatasi"        this.durum = durum        this.tekrarSaniye = tekrarSaniye    }} export async function jsonGetir(adres, basliklar = {}) {    const yanit = await fetch(adres, {        headers: basliklar,        signal: AbortSignal.timeout(2000)    })    if (!yanit.ok) {        const govde = await yanit.text()        const ilkSatir = govde.split("\n")[0].slice(0, 120)        const tekrar = Number(yanit.headers.get("retry-after"))        throw new DisServisHatasi(yanit.status, `${yanit.status} ${ilkSatir}`, tekrar)    }

Sonra yenidenDene o süreye uysun:

disIstek.js
export async function yenidenDene(is, denemeSayisi = 3) {    let bekleme = 500    for (let deneme = 1; deneme <= denemeSayisi; deneme++) {        try {            return await is()        } catch (hata) {            const sure = hata.tekrarSaniye ? hata.tekrarSaniye * 1000 : bekleme            if (deneme === denemeSayisi || !geciciMi(hata) || sure > 10000) {                throw hata            }            console.log(`${deneme}. deneme olmadı (${hata.message}). ${sure} ms sonra yeniden.`)            await bekle(sure)            bekleme = bekleme * 2        }    }}
  • yanit.headers.get("retry-after") başlığı metin olarak verir, başlık yoksa null. Number("3") 3 olur; Number(null) ise, Ders 9'daki Number("") gibi, 0 verir. 0 yanlış sayılan bir değer olduğu için (Ders 12) başlık yoksa sure, artan beklemeye düşer.
  • sure > 10000: servis "bir saat bekle" derse bir saat beklemeyiz. Sunucunu bekleyen bir ziyaretçi var; 10 saniyeden uzun beklemek yerine hatayı hemen yukarı ver ve ziyaretçiye "şu an olmuyor" de.
  • Retry-After bazen saniye yerine bir tarih taşır (Wed, 21 Oct 2026 07:28:00 GMT gibi). Number ondan NaN üretir, NaN da yanlış sayılır; kodumuz o zaman da artan beklemeyle devam eder. Riot'un belgesi süreyi saniye olarak verdiğini söylüyor.

Üç saniye bekle ve deneme.js'i yeniden çalıştır:

Terminal — PowerShell
PS C:\kod\sihirdar-api> node deneme.jsBuyur, taze veri1. istek: 87.344ms1. deneme olmadı (429 {"hata":"Çok sık istek attın"}). 3000 ms sonra yeniden.Buyur, taze veri2. istek: 3.012s1. deneme olmadı (429 {"hata":"Çok sık istek attın"}). 3000 ms sonra yeniden.Buyur, taze veri3. istek: 3.016s

Her 429'dan sonra servisin istediği kadar, 3 saniye beklendi ve ikinci deneme geçti. Sınıra uymak işe yarıyor ama yavaş: üç istek 6 saniyeden fazla sürdü. Asıl çözüm, aynı soruyu hiç sormamak.

Önbellek: aynı soruyu iki kez sorma

Hava durumu 15 dakikada bir güncelleniyor; Open-Meteo'nun cevabındaki interval: 900'ü hatırla. Bir dakika içinde yüz ziyaretçi İstanbul'un havasını sorarsa Open-Meteo'ya yüz kez gitmek israf: cevap yüzünde de aynı. Cevabı bir kez alıp bir süre saklarsın, o süre içindeki her soruya saklananı verirsin. Ders 20'de tarayıcının CSS dosyalarını önbellekte tuttuğunu görmüştün; bu kez önbelleği kendin, sunucunda kuruyorsun.

Önce önbelleği ağsız, tek başına dene. onbellek.js dosyasına yaz; servis yerine sabit bir sürüm döndüren sahte bir fonksiyon kullanıyoruz:

onbellek.js
const onbellek = {} async function onbellekli(anahtar, sureMs, is) {    const kayit = onbellek[anahtar]    if (kayit && Date.now() < kayit.bitis) {        return kayit.veri    }    const veri = await is()    onbellek[anahtar] = { veri: veri, bitis: Date.now() + sureMs }    return veri} function bekle(ms) {    return new Promise((resolve) => setTimeout(resolve, ms))} async function surumGetir() {    console.log("Servise gidiliyor...")    return "16.20.1"} console.log("1:", await onbellekli("surum", 1000, surumGetir))console.log("2:", await onbellekli("surum", 1000, surumGetir))await bekle(1100)console.log("3:", await onbellekli("surum", 1000, surumGetir))
Çıktı
Servise gidiliyor...1: 16.20.12: 16.20.1Servise gidiliyor...3: 16.20.1
  • onbellek düz bir obje (Ders 4): anahtar sorunun kendisi (bir adres gibi), değer { veri, bitis }.
  • bitis, kaydın geçerliliğini yitireceği an: Date.now() + sureMs. Kayıt varsa ve şu an bitişten önceyse servise hiç gitmeden saklananı döndür.
  • Yoksa işi yap, sonucu sakla, döndür. İş hata verirse await is() satırı fırlatır ve kayıt yazılmaz: hatalar önbelleğe girmez.

Çıktıda "Servise gidiliyor" iki kez geçiyor: ilk soruda ve 1,1 saniye sonra, kaydın 1 saniyelik ömrü dolunca. Arada gelen ikinci soru önbellekten cevaplandı.

Yaşam süresi: 10 dakika 10:00 10:03 10:08 10:12 10:16 1. kayıt geçerli: 10:00–10:10 2. kayıt geçerli: 10:12–10:22 süre doldu servise gidildi, cevap 10 dakikalığına saklandı önbellekten cevaplandı, servise gidilmedi
Şekil 30.4 Yaşam süresi 10 dakika olan bir kayıt. 10:00'daki soru servise gider ve cevabı 10:10'a kadar saklar; arada gelen sorular önbellekten cevaplanır. Süre dolduktan sonraki ilk soru (10:12) servise yeniden gider ve yeni bir kayıt açar.

Fonksiyon çalışıyor. onbellek objesini ve onbellekli fonksiyonunu disIstek.js'in sonuna taşı; dışarıdan kullanılacağı için başına export koy:

disIstek.js
const onbellek = {} export async function onbellekli(anahtar, sureMs, is) {    const kayit = onbellek[anahtar]    if (kayit && Date.now() < kayit.bitis) {        return kayit.veri    }    const veri = await is()    onbellek[anahtar] = { veri: veri, bitis: Date.now() + sureMs }    return veri}

Şimdi 429 deneyini önbellekle tekrarla. deneme.js:

deneme.js
import { jsonGetir, yenidenDene, onbellekli } from "./disIstek.js" const adres = "http://localhost:4000/sinirli"console.time("5 istek")for (let i = 1; i <= 5; i++) {    const veri = await onbellekli(adres, 10000, () => yenidenDene(() => jsonGetir(adres)))    console.log(i, veri.mesaj)}console.timeEnd("5 istek")
Terminal — PowerShell
PS C:\kod\sihirdar-api> node deneme.js1 Buyur, taze veri2 Buyur, taze veri3 Buyur, taze veri4 Buyur, taze veri5 Buyur, taze veri5 istek: 90.365ms
İkinci terminal: sahte servis
21:14:17 GET /sinirli

Beş soru, servise tek istek, hiç 429 yok ve toplam 90 milisaniye. Üç fonksiyon iç içe çalıştı: onbellekli önce kendi kaydına bakar, yoksa yenidenDene'yi çağırır, o da jsonGetir'i gerektiği kadar dener. Bir önceki denemeden hemen sonra çalıştırırsan ilk istek bir kez 429 alıp 3 saniye bekleyebilir; sonraki dördü yine önbellekten gelir.

Ara sunucu: hepsi bir arada

Parçalar hazır. Şimdi tarayıcının soracağı uç noktayı yaz. Proje klasöründe app.js:

app.js
import express from "express"import { jsonGetir, yenidenDene, onbellekli } from "./disIstek.js" const app = express()const port = process.env.PORT || 3000 app.use(express.static("public")) app.get("/api/hava", async (req, res) => {    const enlem = Number(req.query.enlem)    const boylam = Number(req.query.boylam)    if (!req.query.enlem || !req.query.boylam || Number.isNaN(enlem) || Number.isNaN(boylam)) {        res.status(400).json({ hata: "enlem ve boylam sayı olmalı" })        return    }    if (enlem < -90 || enlem > 90 || boylam < -180 || boylam > 180) {        res.status(400).json({ hata: "enlem -90 ile 90, boylam -180 ile 180 arasında olmalı" })        return    }    const parametreler = new URLSearchParams({        latitude: enlem,        longitude: boylam,        current: "temperature_2m,wind_speed_10m",        timezone: "auto"    })    const adres = "https://api.open-meteo.com/v1/forecast?" + parametreler    const veri = await onbellekli(adres, 10 * 60 * 1000, () => {        console.log("Open-Meteo'ya gidiliyor:", enlem, boylam)        return yenidenDene(() => jsonGetir(adres))    })    res.json({        sicaklik: veri.current.temperature_2m,        ruzgar: veri.current.wind_speed_10m,        saat: veri.current.time    })}) app.use((req, res) => {    res.status(404).json({ hata: "Böyle bir yol yok" })}) app.use((hata, req, res, next) => {    console.log("Hata:", hata.name, hata.message)    if (hata.name === "TimeoutError") {        res.status(504).json({ hata: "Dış servis zamanında cevap vermedi" })    } else if (hata.durum === 404) {        res.status(404).json({ hata: "Dış serviste böyle bir kayıt yok" })    } else if (hata.name === "DisServisHatasi" || hata.message === "fetch failed") {        res.status(502).json({ hata: "Dış servise şu an ulaşılamıyor" })    } else {        res.status(500).json({ hata: "Sunucuda beklenmeyen bir hata oldu" })    }}) app.listen(port, (hata) => {    if (hata) {        throw hata    }    console.log(`Sihirdar API çalışıyor: http://localhost:${port}`)})
  • express.static("public"), public klasöründeki dosyaları olduğu gibi sunar (Ders 28); birazdan oraya bir sayfa koyacağız.
  • Önce girdiyi doğrula ve gerekirse 400 dön (Ders 28): bozuk bir istek için dış servisin hakkını harcama. Sayıya çeviremediğin değer NaN olur; 41,01 gibi virgüllü bir sayı da 400 alır.
  • Önbelleğin anahtarı, dış servisin tam adresi: aynı koordinat, aynı adres, aynı kayıt. Süre 10 * 60 * 1000, yani 10 dakika.
  • Cevap olarak Open-Meteo'nun bütün JSON'unu değil, sayfanın ihtiyacı olan üç alanı gönderiyoruz. Open-Meteo bir gün alan adlarını değiştirirse yalnız bu satırlar değişir, sayfan değişmez.
  • Express 5, async bir yolun içinden fırlayan hatayı kendiliğinden hata yakalayıcıya gönderir (Ders 28). Yakalayıcı, dış servis hatalarını ziyaretçiye uygun kodlara çevirir: zaman aşımı 504 Gateway Timeout, dış servisin 404'ü 404, öteki dış servis hataları 502 Bad Gateway. Bunlar Ders 26'da "sunucu ya da arkasındaki servis cevap veremiyor" diye gördüğün kodlar. Ayrıntıyı yalnız sunucunun terminaline yaz; ziyaretçiye dış servisin ham hatasını gönderme.

Sunucuyu ilk terminalde başlat. Sahte servis ikinci terminalde açık kalsın; istekleri terminal panelindeki + ile açacağın üçüncü bir terminalden at:

Terminal — PowerShell
PS C:\kod\sihirdar-api> npm run dev > sihirdar-api@1.0.0 dev> node --watch --env-file=.env app.js Sihirdar API çalışıyor: http://localhost:3000
Üçüncü terminal
PS C:\kod\sihirdar-api> curl.exe -i "http://localhost:3000/api/hava?enlem=41.01&boylam=28.98"HTTP/1.1 200 OKX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 56ETag: W/"38-igILVEgswUcNhd7krkPD3cbTwe8"Date: Sun, 11 Oct 2026 18:15:18 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"sicaklik":20.9,"ruzgar":1.9,"saat":"2026-10-11T21:15"}PS C:\kod\sihirdar-api> curl.exe "http://localhost:3000/api/hava?enlem=41.01&boylam=28.98"{"sicaklik":20.9,"ruzgar":1.9,"saat":"2026-10-11T21:15"}PS C:\kod\sihirdar-api> curl.exe "http://localhost:3000/api/hava?enlem=41,01&boylam=28,98"{"hata":"enlem ve boylam sayı olmalı"}PS C:\kod\sihirdar-api> curl.exe "http://localhost:3000/api/hava?enlem=95&boylam=28.98"{"hata":"enlem -90 ile 90, boylam -180 ile 180 arasında olmalı"}PS C:\kod\sihirdar-api> curl.exe http://localhost:3000/api/yok{"hata":"Böyle bir yol yok"}
İlk terminal: ara sunucu
Sihirdar API çalışıyor: http://localhost:3000Open-Meteo'ya gidiliyor: 41.01 28.98

İki hava isteği, Open-Meteo'ya tek gidiş: ikincisi önbellekten geldi. Bozuk girdiler dış servise hiç ulaşmadı.

Dış servis çökerse

Open-Meteo'nun çöktüğünü taklit et: app.js'te adres satırını geçici olarak sahte servisin yavaş yoluyla değiştir ve kaydet. --watch sunucuyu yeniden başlatır, önbellek de boşalır:

app.js
    const adres = "http://localhost:4000/yavas"
Üçüncü terminal
PS C:\kod\sihirdar-api> curl.exe -i "http://localhost:3000/api/hava?enlem=41.01&boylam=28.98"HTTP/1.1 504 Gateway TimeoutX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 48ETag: W/"30-tYCZzkU0XNA23o5TYxH4lvv5nvI"Date: Sun, 11 Oct 2026 18:15:47 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"Dış servis zamanında cevap vermedi"}
İlk terminal: ara sunucu
Open-Meteo'ya gidiliyor: 41.01 28.981. deneme olmadı (The operation was aborted due to timeout). 500 ms sonra yeniden.2. deneme olmadı (The operation was aborted due to timeout). 1000 ms sonra yeniden.Hata: TimeoutError The operation was aborted due to timeout

Cevap 7,6 saniyede geldi: üç deneme, her biri 2 saniyelik süre sınırıyla, aralarda da 1,5 saniye bekleme. Ziyaretçi için uzun bir süre. Deneme sayısını, süre sınırını ve beklemeyi seçerken bu toplamı hep hesapla. Adresi bir de kimsenin dinlemediği bir porta çevir, http://localhost:4001/:

İlk terminal: ara sunucu
Open-Meteo'ya gidiliyor: 41.01 28.981. deneme olmadı (fetch failed). 500 ms sonra yeniden.2. deneme olmadı (fetch failed). 1000 ms sonra yeniden.Hata: TypeError fetch failed

Bu kez curl 502 Bad Gateway ve {"hata":"Dış servise şu an ulaşılamıyor"} aldı. Denemeden sonra adres satırını eski hâline, Open-Meteo'ya geri getir.

Tarayıcıdan sor

Son parça tarayıcı tarafı. Proje klasöründe public adında bir klasör aç ve içine index.html koy:

public/index.html
<!doctype html><html lang="tr"><head>    <meta charset="utf-8">    <meta name="viewport" content="width=device-width, initial-scale=1">    <title>Turnuva günü</title></head><body>    <h1>Turnuva günü: İstanbul</h1>    <p id="hava">Hava durumu yükleniyor...</p>    <script>        async function havayiGoster() {            const kutu = document.querySelector("#hava")            try {                const yanit = await fetch("/api/hava?enlem=41.01&boylam=28.98")                const veri = await yanit.json()                if (!yanit.ok) {                    kutu.textContent = veri.hata                    return                }                kutu.textContent = `${veri.sicaklik} °C, rüzgâr ${veri.ruzgar} km/sa (ölçüm: ${veri.saat})`            } catch (hata) {                kutu.textContent = "Sunucuya ulaşılamadı"            }        }         havayiGoster()    </script></body></html>

fetch'teki adres / ile başlıyor ve alan adı yok: istek, sayfanın geldiği sunucuya, yani senin ara sunucuna gider (Ders 28'deki fetch("/api/sampiyonlar") gibi). Sayfa ile API aynı sunucudan geldiği için CORS'a da takılmaz. Tarayıcıda localhost:3000 adresini aç:

localhost:3000
Tarayıcıda Turnuva günü: İstanbul başlığı, altında 20.9 °C, rüzgâr 1.9 km/sa (ölçüm: 2026-10-11T21:15) satırı.
Gerçek ekran görüntüsü. Sayılar ve saat sende farklı olacak.

F12 ile geliştirici araçlarını aç, Network sekmesine geç ve sayfayı yenile. İstek listesinde sayfanın kendisi ve hava?enlem=41.01&boylam=28.98 var; tarayıcı sekme simgesi için favicon.ico'yu da isteyebilir, sunucun ona 404 döner.

Hepsi localhost:3000'e gidiyor; Open-Meteo'nun adı hiçbir istekte geçmiyor. Tarayıcı dış servisi hiç görmedi. Anahtar isteyen bir servis olsaydı anahtarı da görmeyecekti.

Riot için uç nokta

Aynı kalıpla Riot'un hesap uç noktasını da sarabilirsin. Bu yol anahtar isteyecek; önce, anahtar okunmadıysa sunucu açılırken uyaran bir denetim koy. app.js'te const port satırının altına ekle:

app.js
if (!process.env.RIOT_API_KEY) {    console.log("Uyarı: RIOT_API_KEY yok. --env-file=.env ile mi başlattın?")}

Sonra /api/hava yolunun hemen altına, 404 yakalayıcısından önce yeni yolu yaz:

app.js
app.get("/api/hesap/:ad/:etiket", async (req, res) => {    const adres = "https://europe.api.riotgames.com/riot/account/v1/accounts/by-riot-id/"        + encodeURIComponent(req.params.ad) + "/" + encodeURIComponent(req.params.etiket)    const hesap = await onbellekli(adres, 60 * 60 * 1000, () => {        console.log("Riot'a gidiliyor:", adres)        return yenidenDene(() => jsonGetir(adres, { "X-Riot-Token": process.env.RIOT_API_KEY }))    })    res.json({ ad: hesap.gameName, etiket: hesap.tagLine, puuid: hesap.puuid })})
  • Uyarı satırı, --env-file unutulduğunda Forbidden hatasıyla uğraşmak yerine sorunu ilk saniyede söyler. Program yine açılır; /api/hava'nın anahtara ihtiyacı yok.
  • Tarayıcı Kırmızı Ejder adını K%C4%B1rm%C4%B1z%C4%B1%20Ejder diye kodlayıp gönderir; Express req.params.ad'ı çözüp yeniden Kırmızı Ejder yapar. Riot'a giderken encodeURIComponent ile yeniden kodluyoruz.
  • Önbellek süresi bir saat. Bir hesabın PUUID'si hiç değişmez; oyuncu Riot ID'sini değiştirirse eski kayıt en geç bir saat içinde süresi dolup gider.
  • Riot'un 404'ü ("böyle bir Riot ID yok") hata yakalayıcıda 404 olarak tarayıcıya geçer. Anahtar sorunları (401, 403) ise 502 olur: ziyaretçinin değil, senin sunucunun sorunu.

Uydurma anahtarla dene. Tarayıcıda localhost:3000/api/hesap/Kırmızı Ejder/TR1 adresini de açabilirsin; tarayıcı kodlamayı kendisi yapar:

Üçüncü terminal
PS C:\kod\sihirdar-api> curl.exe -i http://localhost:3000/api/hesap/K%C4%B1rm%C4%B1z%C4%B1%20Ejder/TR1HTTP/1.1 502 Bad GatewayX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 47ETag: W/"2f-vrKOvbM2FjvFULH6H6aBJdxVGDA"Date: Sun, 11 Oct 2026 18:16:27 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"Dış servise şu an ulaşılamıyor"}
İlk terminal: ara sunucu
Riot'a gidiliyor: https://europe.api.riotgames.com/riot/account/v1/accounts/by-riot-id/K%C4%B1rm%C4%B1z%C4%B1%20Ejder/TR1Hata: DisServisHatasi 401 {"status":{"message":"Unknown apikey","status_code":401}}

Riot 401 dedi; ziyaretçi yalnız 502 ve genel bir cümle gördü, ayrıntı senin terminalinde kaldı. Geçerli bir anahtarla ve var olan bir Riot ID ile cevap {"ad":"...","etiket":"TR1","puuid":"..."} biçiminde gelir.

Şimdi uyarıyı gör. İlk terminalde sunucuyu Ctrl+C ile durdur ve bu kez --env-file olmadan başlat:

Terminal — PowerShell
PS C:\kod\sihirdar-api> node app.jsUyarı: RIOT_API_KEY yok. --env-file=.env ile mi başlattın?Sihirdar API çalışıyor: http://localhost:3000

Denedikten sonra yine Ctrl+C ile durdur ve npm run dev ile yeniden başlat. Sunucu açıkken ikinci bir kopyayı başka bir terminalde başlatırsan 3000 numaralı kapı dolu olduğu için EADDRINUSE ile durur (Ders 28).

Görevler

Görevleri sihirdar-api klasöründe yap. Sunucu görevlerinde npm run dev açık kalsın, istekleri ayrı bir terminalden curl.exe ile at ve hem cevaba hem sunucunun terminaline bak. Önce kendin dene, takılırsan ipuçlarını sırayla aç.

Görev 1

Belgeden yarının tahmini

Open-Meteo belgesinde günlük (daily) değişkenleri bul ve yarin.js dosyasını yaz. Ankara (enlem 39.93, boylam 32.86) için yarının en düşük ve en yüksek sıcaklığını şöyle yazdırsın:

Metin
Yarın (2026-10-12) Ankara: en düşük 11.4 °C, en yüksek 23.5 °C

Tarih ve sayılar sende farklı olacak. Bitince sınırı da dene: tahmini bir güne indirirsen ne olur?

İpucu 1

Belgede Daily Weather Variables listesine bak; sıcaklığın günlük en yükseği ve en düşüğü için iki ayrı ad var. Kaç günlük tahmin geleceğini forecast_days parametresi belirler.

İpucu 2

daily: "temperature_2m_max,temperature_2m_min", forecast_days: "2" ve timezone: "auto". Yanıtta daily.time, daily.temperature_2m_max ve daily.temperature_2m_min birer dizi: 0 numaralı eleman bugün, 1 numaralı yarın.

İpucu 3

Belge, günlük değişkenler için timezone'u zorunlu sayıyor. Denediğimizde vermeyince hata gelmedi, ama günler GMT'ye göre bölündü ("timezone":"GMT"); Türkiye saatiyle gece yarısından sonraki üç saat yanlış güne düşer. Belgenin söylediğini yap.

Çözümü göster
yarin.js
const parametreler = new URLSearchParams({    latitude: "39.93",    longitude: "32.86",    daily: "temperature_2m_max,temperature_2m_min",    forecast_days: "2",    timezone: "auto"})const yanit = await fetch("https://api.open-meteo.com/v1/forecast?" + parametreler)const veri = await yanit.json() const gun = veri.daily.time[1]const enDusuk = veri.daily.temperature_2m_min[1]const enYuksek = veri.daily.temperature_2m_max[1]console.log(`Yarın (${gun}) Ankara: en düşük ${enDusuk} °C, en yüksek ${enYuksek} °C`)
Çıktıörnek, sende farklı çıkabilir
Yarın (2026-10-12) Ankara: en düşük 11.4 °C, en yüksek 23.5 °C

Sınır: forecast_days "1" olunca diziler tek elemanlı kalır ve [1] undefined verir. Çıktı Yarın (undefined) Ankara: en düşük undefined °C, en yüksek undefined °C olur: hata yok ama sonuç yanlış. Gerçek bir programda veri.daily.time.length < 2 ise anlaşılır bir mesaj yazarsın. Belgedeki üst sınırı da dene: forecast_days en çok 16; 17 yazınca 400 ve Forecast days is invalid. Allowed range 0 to 16. ile başlayan bir sebep gelir.

Görev 2

Data Dragon'a süre sınırı

surum.js dosyasında Data Dragon'dan sürüm listesini (https://ddragon.leagueoflegends.com/api/versions.json) en çok 1,5 saniye bekleyerek al ve en yeni sürümü yazdır:

Metin
En yeni sürüm: 16.20.1

Süre dolarsa Data Dragon 1,5 saniyede cevap vermedi, başka bir sorun olursa Sürüm alınamadı: ve hatanın mesajını yazsın. Başarısız bir durum kodunu da hata say. Üç durumu da kendin üret: süreyi 1 yap, adresteki versions.json'u surumler.json yap, alan adını .cmo ile bitir.

İpucu 1

fetch'in ikinci değerine { signal: AbortSignal.timeout(1500) } ver ve her şeyi bir try içine al.

İpucu 2

catch içinde zaman aşımını hata.name === "TimeoutError" ile ayır. yanit.ok false ise throw new Error("Durum kodu " + yanit.status) ile hatayı kendin fırlat; o da aynı catch'e düşer.

Çözümü göster
surum.js
const adres = "https://ddragon.leagueoflegends.com/api/versions.json" try {    const yanit = await fetch(adres, { signal: AbortSignal.timeout(1500) })    if (!yanit.ok) {        throw new Error("Durum kodu " + yanit.status)    }    const surumler = await yanit.json()    console.log("En yeni sürüm: " + surumler[0])} catch (hata) {    if (hata.name === "TimeoutError") {        console.log("Data Dragon 1,5 saniyede cevap vermedi")    } else {        console.log("Sürüm alınamadı: " + hata.message)    }}
Çıktıörnek, sende farklı çıkabilir
En yeni sürüm: 16.20.1

Üç denemenin gerçek çıktıları:

  • Süre 1 → Data Dragon 1,5 saniyede cevap vermedi. Mesajdaki süre sabit yazıldığı için değişmedi; süreyi bir değişkende tutup mesajda da onu kullanırsan ikisi hep birlikte değişir.
  • surumler.json → Sürüm alınamadı: Durum kodu 403. Data Dragon olmayan dosyalara 404 değil 403 döndürür (Ders 15).
  • .cmo → Sürüm alınamadı: fetch failed.

Sınır: AbortSignal.timeout(0) isteği hemen keser ve TimeoutError verir. Eksi bir süre ise hiç istek atılmadan bir RangeError fırlatır; catch onu da yakalar: Sürüm alınamadı: The value of "delay" is out of range. It must be >= 0 && <= 4294967295. Received -1.

Görev 3

Beklemeye tavan

yenidenDene'de bekleme her seferinde ikiye katlanıyor; 6 denemede son bekleme 8 saniyeye çıkar. Beklemeye 2000 ms tavan koy. Önce bekleme.js dosyasında, kaçıncı başarısız denemeden sonra ne kadar bekleneceğini veren beklemeSuresi(deneme) fonksiyonunu yaz ve şu tabloyu yazdır:

Metin
1. denemeden sonra 500 ms2. denemeden sonra 1000 ms3. denemeden sonra 2000 ms4. denemeden sonra 2000 ms5. denemeden sonra 2000 ms

Sonra aynı tavanı disIstek.js'teki yenidenDene'ye uygula.

İpucu 1

Fonksiyonun içinde 500'den başla ve deneme - 1 kez ikiyle çarp. Bir for döngüsü yeter.

İpucu 2

İki sayıdan küçüğünü Math.min verir (Ders 9): Math.min(sure, 2000).

Çözümü göster
bekleme.js
function beklemeSuresi(deneme) {    let sure = 500    for (let i = 1; i < deneme; i++) {        sure = sure * 2    }    return Math.min(sure, 2000)} for (let deneme = 1; deneme <= 5; deneme++) {    console.log(`${deneme}. denemeden sonra ${beklemeSuresi(deneme)} ms`)}
Çıktı
1. denemeden sonra 500 ms2. denemeden sonra 1000 ms3. denemeden sonra 2000 ms4. denemeden sonra 2000 ms5. denemeden sonra 2000 ms

yenidenDene'de değişen tek satır: bekleme = Math.min(bekleme * 2, 2000). Sınırlar: 3. denemede değer tam tavana, 2000'e eşit; tavan onu değiştirmiyor, yalnız aşanları kesiyor. denemeSayisi 1 olursa döngü bir kez döner ve hiç beklenmez: ilk hata doğrudan yukarı çıkar.

Görev 4

Sürüm uç noktası

app.js'e GET /api/surum yolunu ekle. Data Dragon'dan en yeni sürümü {"surum":"16.20.1"} biçiminde döndürsün ve cevabı bir saat önbellekte tutsun. Art arda üç istekte sunucunun terminalinde Data Dragon'a gidiliyor satırı yalnız bir kez görünmeli.

İpucu 1

/api/hava'daki kalıbı kullan: onbellekli(adres, süre, () => yenidenDene(() => jsonGetir(adres))). versions.json bir dizi; en yeni sürüm [0].

İpucu 2

Bir saat milisaniye olarak 60 * 60 * 1000. Yeni yolu 404 yakalayıcısından önce yaz; sonra yazarsan her istek {"hata":"Böyle bir yol yok"} alır.

Çözümü göster
JavaScript
app.get("/api/surum", async (req, res) => {    const adres = "https://ddragon.leagueoflegends.com/api/versions.json"    const surumler = await onbellekli(adres, 60 * 60 * 1000, () => {        console.log("Data Dragon'a gidiliyor")        return yenidenDene(() => jsonGetir(adres))    })    res.json({ surum: surumler[0] })})
Terminal — PowerShell
PS C:\kod\sihirdar-api> curl.exe http://localhost:3000/api/surum{"surum":"16.20.1"}PS C:\kod\sihirdar-api> curl.exe http://localhost:3000/api/surum{"surum":"16.20.1"}PS C:\kod\sihirdar-api> curl.exe http://localhost:3000/api/surum{"surum":"16.20.1"}
Sunucunun terminali
Sihirdar API çalışıyor: http://localhost:3000Data Dragon'a gidiliyor

Sınır: süreyi 0 yaparsan Date.now() < kayit.bitis hiç doğru olmaz ve her istek Data Dragon'a gider; terminalde satırı üç kez görürsün. Denerken önbelleği kapatmanın en kısa yolu bu.

Görev 5

Anahtarlı servis

Gerçek bir Riot anahtarın olmasa da anahtarı sunucuda tutup başlıkta göndermeyi baştan sona deneyebilirsin. Sahte servis, anahtar isteyen bir servisin rolünü oynasın. sahte-servis.js'te app.listen satırının üstüne şu yolu ekle; bu kısım hazır, senin işin ara sunucu tarafı:

sahte-servis.js
app.get("/gizli", (req, res) => {    if (req.get("X-Sahte-Anahtar") !== "lux-123") {        res.status(401).json({ hata: "Anahtar yanlış" })        return    }    res.json({ mesaj: "Anahtar doğru, hoş geldin" })})

req.get("X-Sahte-Anahtar") gelen isteğin bu adlı başlığını okur; başlık yoksa undefined verir. Şimdi .env'e SAHTE_ANAHTAR=lux-123 satırını ekle ve app.js'e GET /api/gizli yolunu yaz. Yol anahtarı .env'den alsın, X-Sahte-Anahtar başlığında göndererek http://localhost:4000/gizli adresine sorsun ve gelen mesajı tarayıcıya iletsin:

Metin
{"mesaj":"Anahtar doğru, hoş geldin"}

Sınırları da dene: önce .env'deki anahtarı lux-124 yap; sonra anahtarı düzeltip app.js'teki başlığın adını X-Sahte-Anahtr diye yanlış yaz. İki durumda da ziyaretçi ne görüyor, sunucunun terminali ne yazıyor? En sonda tarayıcıda localhost:3000/api/gizli adresini aç ve Network sekmesinde anahtarı ara.

İpucu 1

Riot uç noktasındaki kalıbı kullan: jsonGetir(adres, { "X-Riot-Token": process.env.RIOT_API_KEY }). Başlığın ve ortam değişkeninin adını değiştir.

İpucu 2

npm run dev .env'i de izler: dosyayı kaydedince sunucu kendiliğinden yeniden başlar. Sahte servis --watch ile açık olduğu için yeni yolu kaydetmen ona da yeter.

İpucu 3

Servisin 401'i jsonGetir'de bir DisServisHatasi olarak fırlar. Onun için yeni bir şey yazman gerekmez; hata yakalayıcın onu zaten bir durum koduna çeviriyor. Hangisine?

Çözümü göster
JavaScript
app.get("/api/gizli", async (req, res) => {    const veri = await jsonGetir("http://localhost:4000/gizli", {        "X-Sahte-Anahtar": process.env.SAHTE_ANAHTAR    })    res.json({ mesaj: veri.mesaj })})
Üçüncü terminal
PS C:\kod\sihirdar-api> curl.exe http://localhost:3000/api/gizli{"mesaj":"Anahtar doğru, hoş geldin"}PS C:\kod\sihirdar-api> curl.exe http://localhost:3000/api/gizli{"hata":"Dış servise şu an ulaşılamıyor"}

İkinci istek, .env'de lux-124 yazarken atıldı. O sırada sunucunun terminali:

İlk terminal: ara sunucu
Change detected in 'C:\\kod\\sihirdar-api\\.env'Restarting 'app.js'Sihirdar API çalışıyor: http://localhost:3000Hata: DisServisHatasi 401 {"hata":"Anahtar yanlış"}

Servis 401 dedi, ziyaretçi 502 ve genel bir cümle aldı; ayrıntı yalnız senin terminalinde. Başlığın adını yanlış yazınca da sonuç aynı: anahtar hiç gitmez, servis 401 der. Network sekmesindeki gizli isteğine tıkla: ne başlıklarında ne cevabında lux-123 geçiyor. Anahtar sunucudan hiç çıkmadı.

Görev 6

Şehir adıyla hava

GET /api/sehir/:ad yolunu ekle. Küçük bir tabloda üç şehrin koordinatı dursun: İstanbul (41.01, 28.98), Ankara (39.93, 32.86), İzmir (38.42, 27.14). Bilinen şehir için {"sehir":"izmir","sicaklik":...,"ruzgar":...,"saat":"..."} dönsün, bilinmeyene 404 ve {"hata":"Bu şehri tanımıyorum: Bursa"}. Tarayıcıda İzmir ve İZMİR yazınca da çalışsın. Hava isteğinin kodunu /api/hava ile iki kez yazma.

Sınır değerleri: izmir, İzmir, İZMİR, IZMIR, Bursa ve constructor.

İpucu 1

Hava isteğini bir fonksiyona çıkar: async function havaGetir(enlem, boylam) önbellekli isteği yapıp { sicaklik, ruzgar, saat } döndürsün. /api/hava da /api/sehir/:ad de onu çağırsın.

İpucu 2

Tabloya bakmadan önce adı küçült: req.params.ad.toLocaleLowerCase("tr") (Ders 9). Türkçe kuralla İZMİR izmir olur. IZMIR ise noktasız ı ile ızmır olur ve bulunamaz; bu doğru bir davranış, çünkü Türkçede I'nın küçüğü ı.

İpucu 3

Adı sehirler[anahtar] ile doğrudan sorma: her objenin kalıtımla gelen constructor gibi alanları vardır ve sehirler["constructor"] boş değildir. Önce Object.keys(sehirler).includes(anahtar) (Ders 12) ile denetle.

Çözümü göster
JavaScript
async function havaGetir(enlem, boylam) {    const parametreler = new URLSearchParams({        latitude: enlem,        longitude: boylam,        current: "temperature_2m,wind_speed_10m",        timezone: "auto"    })    const adres = "https://api.open-meteo.com/v1/forecast?" + parametreler    const veri = await onbellekli(adres, 10 * 60 * 1000, () => {        console.log("Open-Meteo'ya gidiliyor:", enlem, boylam)        return yenidenDene(() => jsonGetir(adres))    })    return {        sicaklik: veri.current.temperature_2m,        ruzgar: veri.current.wind_speed_10m,        saat: veri.current.time    }} const sehirler = {    istanbul: { enlem: 41.01, boylam: 28.98 },    ankara: { enlem: 39.93, boylam: 32.86 },    izmir: { enlem: 38.42, boylam: 27.14 }} app.get("/api/sehir/:ad", async (req, res) => {    const anahtar = req.params.ad.toLocaleLowerCase("tr")    if (!Object.keys(sehirler).includes(anahtar)) {        res.status(404).json({ hata: `Bu şehri tanımıyorum: ${req.params.ad}` })        return    }    const sehir = sehirler[anahtar]    const hava = await havaGetir(sehir.enlem, sehir.boylam)    res.json({ sehir: anahtar, ...hava })})

/api/hava'nın içi de kısalır: iki doğrulamadan sonra parametreleri kuran ve Open-Meteo'ya giden satırların yerine tek satır kalır, res.json(await havaGetir(enlem, boylam)). ...hava, Ders 12'deki yayma: havaGetir'in döndürdüğü üç alanı yeni objeye kopyalar. Tarayıcıda İzmir yazınca tarayıcı İ'yi %C4%B0 diye kodlar; curl.exe ile denerken kodlanmış hâlini yaz:

Terminal — PowerShell
PS C:\kod\sihirdar-api> curl.exe http://localhost:3000/api/sehir/istanbul{"sehir":"istanbul","sicaklik":20.8,"ruzgar":2.2,"saat":"2026-10-11T21:30"}PS C:\kod\sihirdar-api> curl.exe http://localhost:3000/api/sehir/%C4%B0ZM%C4%B0R{"sehir":"izmir","sicaklik":20.5,"ruzgar":0.4,"saat":"2026-10-11T21:30"}PS C:\kod\sihirdar-api> curl.exe -i http://localhost:3000/api/sehir/IZMIRHTTP/1.1 404 Not FoundX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 41ETag: W/"29-xpe4di0UEkFrNaNE34KPsoztPLU"Date: Sun, 11 Oct 2026 18:30:36 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"Bu şehri tanımıyorum: IZMIR"}PS C:\kod\sihirdar-api> curl.exe http://localhost:3000/api/sehir/constructor{"hata":"Bu şehri tanımıyorum: constructor"}

Bu denemeden önce /api/hava ile İstanbul'u sormuştuk; sunucunun terminalinde İstanbul için yeni bir Open-Meteo'ya gidiliyor satırı çıkmadı. İki yol aynı adresi kurduğu için aynı önbellek kaydını paylaştı. Object.keys denetimi olmasaydı constructor isteği tabloda bir şey bulmuş sayılır, koordinatsız bir istek Open-Meteo'ya gider ve ziyaretçi anlamsız bir 502 alırdı.

Kendini sına

Soru 1

Hangilerini yeniden denersin? 400, 401, 404, 429, 503, zaman aşımı, fetch failed

Cevabı göster

429 (söylenen süre kadar bekleyip), 503, zaman aşımı ve fetch failed. 400 ve 404 isteğin yanlış olduğunu, 401 anahtarın yanlış olduğunu söyler; aynı isteği yeniden göndermek sonucu değiştirmez. Bir incelik: fetch failed her zaman geçici değildir. Alan adını yanlış yazdıysan (ENOTFOUND) yeniden denemek zararsız ama boşunadır; ayırmak istersen hatanın cause alanındaki koda bakarsın.

Soru 2

Arkadaşın node riot.js ile çalıştırıyor ve { status: { message: 'Forbidden', status_code: 401 } } alıyor. .env'de doğru anahtar var. Sorun ne?

Cevabı göster

--env-file=.env unutulmuş. .env okunmadığı için process.env.RIOT_API_KEY undefined, fetch da başlığa undefined metnini koydu; Riot anahtara benzemeyen bu metne Forbidden dedi. Doğrusu node --env-file=.env riot.js. Dersteki ara sunucu bu durumu açılışta Uyarı: RIOT_API_KEY yok diye söylüyor.

Soru 3

Önbelleğin yaşam süresi 10 dakika. Aynı soru 14:00, 14:04, 14:09, 14:11 ve 14:15'te geliyor. Dış servise kaç kez gidilir?

Cevabı göster

İki kez. 14:00'te gidilir ve kayıt 14:10'a kadar geçerli olur; 14:04 ve 14:09 önbellekten cevaplanır. 14:11'de süre dolmuştur: yeniden gidilir, yeni kayıt 14:21'e kadar geçerlidir. 14:15 önbellekten cevaplanır.

Soru 4

yenidenDene 4 deneme yapacak şekilde çağrılıyor (denemeSayisi 4), ilk bekleme 500 ms ve her seferinde ikiye katlanıyor. Servis hiç cevap vermiyor; her deneme 2 saniyelik zaman aşımıyla bitiyor. Ziyaretçi hata cevabını kaç saniye sonra alır?

Cevabı göster

11,5 saniye sonra. Dört deneme × 2 saniye = 8 saniye; aralarda üç bekleme: 500 + 1000 + 2000 = 3,5 saniye. Son denemeden sonra beklenmez. Dersteki üç denemeli ayarla bu süre 7,6 saniye çıkmıştı. Deneme sayısı, süre sınırı ve bekleme birlikte ziyaretçinin ne kadar bekleyeceğini belirler; bu toplamı hep hesapla.

Soru 5

Mesajı oku: ne oldu, ilk neye bakarsın?

Hata çıktısı
SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON    at JSON.parse (<anonymous>)    at parseJSONFromBytes (node:internal/deps/undici/undici:4394:19)
Cevabı göster

yanit.json() bir HTML sayfasını JSON sanıp çevirmeye çalıştı; < HTML'in ilk harfi. Önce adrese bak: API'nin adresi yerine belgenin ya da sitenin adresini mi yazdın, api. eksik mi? Sonra durum koduna ve Content-Type'a bak. jsonGetir ikisini de json()'dan önce denetlediği için bu hatayı hiç göstermez, yerine JSON yerine text/html; charset=utf-8 geldi der.

Soru 6

Anahtarı sunucudaki .env'e koydun. Sunucun GET /api/anahtar diye bir yolla anahtarı sayfaya gönderiyor, sayfa da Riot'a kendisi soruyor. Sorun ne?

Cevabı göster

Anahtar yine tarayıcıya gidiyor; nereden geldiğinin önemi yok. Network sekmesine bakan herkes /api/anahtar'ın cevabında ve Riot isteğinin başlığında anahtarı görür. Ara sunucunun işi anahtarı dağıtmak değil, anahtarla soruyu kendisi sormak ve tarayıcıya yalnız cevabı vermek.

Soru 7

Bir servis 429 ve Retry-After: 120 döndürdü. Dersteki yenidenDene ne yapar? Bu neden iyi, neyi eksik bırakıyor?

Cevabı göster

Bekleme süresi 120 * 1000 = 120000 ms çıkar. sure > 10000 olduğu için hiç beklemeden hatayı fırlatır, hata yakalayıcı da ziyaretçiye hemen 502 döner. İki dakika boyunca bir ziyaretçiyi bekletmek yerine hemen cevap vermek iyi. Eksik olan şu: sıradaki ziyaretçinin isteği servise yine gider ve yine 429 alır. Gerçek bir uygulamada "şu saate kadar sorma" bilgisini de saklarsın ve o süre dolana kadar servise hiç gitmezsin.

Kayıt noktası

Projeyi Git'e kaydet. sihirdar-api yeni bir proje klasörü olduğu için önce git init. Sunucuları durdurmana gerek yok; komutları üçüncü terminalde yaz:

Bir sonraki derste kendi API'nin kapısına kilit takacaksın: kullanıcı girişi, şifreleri güvenle saklamak, korunan yollar ve en sık güvenlik açıkları. Ders 31: Kimlik doğrulama ve güvenlik temelleri.

İşaret yalnızca bu tarayıcıda saklanır, kimseye gönderilmez.