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
fetchile.env'deki anahtarı başlıkta gönderebilir;401,429,5xxve JSON olmayan yanıtları tek bir fonksiyonda ayırt edebilirsin AbortSignal.timeoutile isteğe süre sınırı koyabilir, geçici hatalarda artan beklemeyle yeniden deneyebilirsin429veRetry-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
- Anahtar tarayıcıya girmez
- API belgesi okumak
- Sunucuda fetch
- Riot Games API'si: anahtar, sınırlar, bölgeler
- Anahtarı başlıkta göndermek
- Zaman aşımı: sonsuza dek bekleme
- Yeniden denemek ve artan bekleme
- Hız sınırı: 429 ve Retry-After
- Önbellek: aynı soruyu iki kez sorma
- Ara sunucu: hepsi bir arada
- Görevler
- Kendini sına
- 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:
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:
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.
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:
| Soru | Belgede 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:
| Soru | Open-Meteo'nun cevabı |
|---|---|
| Adres | Temel adres https://api.open-meteo.com, uç nokta /v1/forecast |
| Parametreler | latitude (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. |
| Anahtar | Ticari olmayan kullanımda yok |
| Sınır | Günde 10.000'den az, saatte 5.000, dakikada 600 istek |
| Cevap ve hata | JSON; 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:
PS C:\kod> curl.exe "https://api.open-meteo.com/v1/forecast?latitude=41.01&longitude=28.98¤t=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:
{ "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:
PS C:\kod> curl.exe -i "https://api.open-meteo.com/v1/forecast?latitude=41.01&longitude=28.98¤t=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:
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 vulnerabilitiesBu 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:
{ "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:
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.
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`)PS C:\kod\sihirdar-api> node hava.jshttps://api.open-meteo.com/v1/forecast?latitude=41.01&longitude=28.98¤t=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/saSayılar sende farklı çıkacak. İlk satır, URLSearchParams'ın kurduğu adres.
- Ders 25'te adresteki sorguyu okumak için kullandığın
URLSearchParamstersini de yapar: bir objeden sorgu metni kurar ve özel işaretleri kodlar. Çıktının ilk satırını sağa kaydırırsancurrent=temperature_2m%2Cwind_speed_10mkısmını görürsün: virgül%2Coldu. Metne eklenince kendiliğindenlatitude=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. fetchNode'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.okdenetimini ş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:
developer.riotgames.comadresine gir ve Login ile Riot hesabınla, yani oyuna girdiğin hesapla giriş yap.- 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. - 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:
| Anahtar | Ne için? | Süre ve sınır |
|---|---|---|
| Geliştirici (development) | Kendi bilgisayarında denemek, prototip yapmak | 24 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ınRetry-Afterbaş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 adres | Hangi veriler? |
|---|---|---|
| Platform (sunucu) | https://tr1.api.riotgames.com | Bir sunucuya ait veriler: sihirdar bilgisi, lig sıralaması |
| Bölge | https://europe.api.riotgames.com | Hesaplar (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:
Belgede yanıt kodlarının listesi de var. Hangisinde ne yapacağın daha sonra yeniden deneme kurarken işine yarayacak:
| Kod | Belgenin açıklaması | Yeniden dene? |
|---|---|---|
400 | İstek bozuk: eksik ya da hatalı parametre | Hayır, isteği düzelt |
401 | Anahtar gönderilmedi | Hayır |
403 | Anahtar geçersiz ya da engellenmiş, ya da yol yanlış | Hayır |
404 | Aranan kayıt yok | Hayır |
415 | Content-Type başlığı yanlış | Hayır |
429 | Hız sınırı aşıldı | Evet, Retry-After kadar bekleyip |
500 | Sunucuda beklenmeyen hata | Evet |
503 | Servis geçici olarak kullanılamıyor | Evet |
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.
RIOT_API_KEY=RGAPI-12345678-abcd-4ef0-9876-0123456789abSonra riot.js dosyasını yaz. Kendi Riot ID'n varsa ad ile etiket'e onu yaz:
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.headersobjesi 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.
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.
200{ puuid: 'aB3x…78 karakterlik kimlik…Qz9', gameName: 'Sihirdar', tagLine: 'TR1'}{ 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.
{ status: { message: 'Forbidden', status_code: 401 } }: anahtar yerine undefined gittiNe 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.
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.
Cannot process request apikey or authorization header is emptyNe zaman çıkar? Başlık hiç gitmeyince. En sık sebep headers yerine header yazmak: fetch tanımadığı ayarı hata vermeden yok sayar.
const yanit = await fetch(adres, { header: { "X-Riot-Token": process.env.RIOT_API_KEY }})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ç:
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ünedurumalanı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:
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)}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.
SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSONNe 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:
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)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.0Durum 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:
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)}PS C:\kod\sihirdar-api> node deneme.jsJSON yerine text/html; charset=utf-8 geldiAdresteki 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ç:
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./yavasher isteği 3 saniye bekletip öyle cevaplar.- Ara katman (Ders 28) gelen her isteği saatiyle yazar:
21:12:41 GET /yavasgibi.toLocaleTimeString, Ders 17'dekitoLocaleString'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:
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:
console.time("süre")const yanit = await fetch("http://localhost:4000/yavas")console.log(await yanit.json())console.timeEnd("süre")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:
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")DOMException [TimeoutError]: The operation was aborted due to timeoutNe zaman çıkar? AbortSignal.timeout ile verdiğin süre dolunca istek kesilir ve fetch bu hatayı fırlatır. Yakalamazsan program çöker:
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.0Mesaj: "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.
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")PS C:\kod\sihirdar-api> node yavas.jsTimeoutError: The operation was aborted due to timeoutsüre: 2.005sProgram tam 2 saniyede vazgeçti. Sahte servisin terminaline bak:
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:
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, 404 | Hayır | İstek yanlış; aynısı yine yanlış olur. |
401, 403 | Hayır | Anahtar sorunu; beklemek düzeltmez. |
429 | Evet, söylenen kadar bekleyip | Sınır doldu, zamanla açılır. |
500, 502, 503, 504 | Evet | Servis geçici olarak sorunlu. |
Zaman aşımı, fetch failed | Evet | Ağ 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:
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:
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.yenidenDeneonu 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 olursacatch'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:
import { jsonGetir, yenidenDene } from "./disIstek.js" console.time("toplam")const veri = await yenidenDene(() => jsonGetir("http://localhost:4000/kararsiz"))console.log(veri)console.timeEnd("toplam")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.641s21:13:02 GET /kararsiz21:13:02 GET /kararsiz21:13:03 GET /kararsizServis üç 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: 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:
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.0Hiç "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.
return await yerine returnNe zaman çıkar? try içinden bir Promise'i await yazmadan döndürünce. Fark tek bir sözcük:
try { return is() } catch (hata) {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.0Sahte 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.
TypeError: fetch failed ve altında ECONNREFUSEDNe 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.
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.0fetch 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:
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çmediyse429.res.set("Retry-After", ...)yanıta bir başlık ekler. Değer saniye cinsinden bir metin;Math.ceilyukarı yuvarlar, 2,4 saniye kaldıysa3yazar.- 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:
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:
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`)}DisServisHatasi: 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:
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:
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:
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 yoksanull.Number("3")3olur;Number(null)ise, Ders 9'dakiNumber("")gibi,0verir.0yanlış sayılan bir değer olduğu için (Ders 12) başlık yoksasure, 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-Afterbazen saniye yerine bir tarih taşır (Wed, 21 Oct 2026 07:28:00 GMTgibi).NumberondanNaNüretir,NaNda 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:
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.016sHer 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:
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))Servise gidiliyor...1: 16.20.12: 16.20.1Servise gidiliyor...3: 16.20.1onbellekdü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ı.
Fonksiyon çalışıyor. onbellek objesini ve onbellekli fonksiyonunu disIstek.js'in sonuna taşı; dışarıdan kullanılacağı için başına export koy:
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:
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")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.365ms21:14:17 GET /sinirliBeş 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:
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"),publicklasöründeki dosyaları olduğu gibi sunar (Ders 28); birazdan oraya bir sayfa koyacağız.- Önce girdiyi doğrula ve gerekirse
400dön (Ders 28): bozuk bir istek için dış servisin hakkını harcama. Sayıya çeviremediğin değerNaNolur;41,01gibi virgüllü bir sayı da400alı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,
asyncbir 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ış servisin404'ü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:
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:3000PS 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"}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:
const adres = "http://localhost:4000/yavas"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"}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 timeoutCevap 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/:
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 failedBu 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:
<!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ç:

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:
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.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-fileunutulduğundaForbiddenhatası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ı EjderadınıK%C4%B1rm%C4%B1z%C4%B1%20Ejderdiye kodlayıp gönderir; Expressreq.params.ad'ı çözüp yenidenKırmızı Ejderyapar. Riot'a giderkenencodeURIComponentile 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ıda404olarak tarayıcıya geçer. Anahtar sorunları (401,403) ise502olur: 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:
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"}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:
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:3000Denedikten 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ç.
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:
Yarın (2026-10-12) Ankara: en düşük 11.4 °C, en yüksek 23.5 °CTarih 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
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`)Yarın (2026-10-12) Ankara: en düşük 11.4 °C, en yüksek 23.5 °CSı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.
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:
En yeni sürüm: 16.20.1Sü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
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) }}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 dosyalara404değil403dö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.
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:
1. denemeden sonra 500 ms2. denemeden sonra 1000 ms3. denemeden sonra 2000 ms4. denemeden sonra 2000 ms5. denemeden sonra 2000 msSonra 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
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`)}1. denemeden sonra 500 ms2. denemeden sonra 1000 ms3. denemeden sonra 2000 ms4. denemeden sonra 2000 ms5. denemeden sonra 2000 msyenidenDene'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.
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
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] })})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"}Sihirdar API çalışıyor: http://localhost:3000Data Dragon'a gidiliyorSı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.
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ı:
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:
{"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
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 })})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:
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ı.
Ş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
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:
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?
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.