Blok 4 · Ders 29
Dosya sistemi, ortam değişkenleri ve gizli bilgiler
Veriyi dosyada kalıcı tutmak, ayarları ortam değişkenlerine taşımak ve şifreleri asla koda ya da yayına koymamak.
Bu dersin sonunda
node:fs/promisesile dosya okuyup yazabilir, klasör açıp içindekileri listeleyebilirsinnode:pathveimport.meta.dirnameile, programı hangi klasörden başlatırsan başlat doğru dosyayı bulan yollar kurabilirsin- Express API'nin verisini bir JSON dosyasında kalıcı tutabilir; yarım kalan dosyaya ve aynı anda gelen isteklere karşı güvenli yazabilirsin
- Ayarları
process.env'den okuyabilir,.envdosyasınınode --env-fileile yükleyip eksik ya da bozuk ayarı açık bir hatayla yakalayabilirsin - Gizli bilgileri
.gitignoreile Git'ten, ayrı birpublicklasörüyle internetten uzak tutabilir; sızan bir anahtarda ne yapacağını sırasıyla sayabilirsin - Geliştirme ve canlı ortam için ayarları ayırıp
NODE_ENVile programın canlıda farklı davranmasını sağlayabilirsin
Bu sayfada
- Bellek unutur, disk hatırlar
- node:fs/promises: dosya ve klasör
- Dosya yolları: node:path ve import.meta.dirname
- JSON dosyası: API'nin küçük veritabanı
- Ayarlar koddan çıksın: ortam değişkenleri
- .env dosyası ve node --env-file
- Ayarları tek yerde toplamak: ayarlar.js
- Gizli bilgiler: koda ve Git'e girmez
- Yayına giden klasörde gizli dosya kalmasın
- Geliştirme ve canlı ortam
- Görevler
- Kendini sına
- Kayıt noktası
Ders 27 ve 28'de yazdığın API'nin bir zayıflığı vardı: sunucu kapanınca bütün veri gidiyordu. Bu derste veriyi bir dosyaya yazıp kalıcı yapacaksın ve bunu yaparken çıkan iki gerçek tehlikeyi, yarım kalan dosyayı ve aynı anda gelen istekleri, kendi gözünle göreceksin. Sonra port ve şifre gibi ayarları koddan çıkarıp .env dosyasına taşıyacak, o dosyayı Git'ten ve internetten nasıl uzak tutacağını öğreneceksin. Dersin sonunda yeniden başlatılınca verisini unutmayan, ayarlarını ortamdan okuyan ve hiçbir şifresi koda ya da GitHub'a girmeyen bir API'n olacak.
Bellek unutur, disk hatırlar
Programının değişkenleri bilgisayarın belleğinde (RAM) durur. Program kapanınca bellekteki her şey silinir; Ders 27'deki şampiyon dizisi de bu yüzden kayboldu. Disk ise program kapansa da, bilgisayar kapansa da hatırlar. Bir bilgiyi programdan uzun yaşatmak istiyorsan onu diske, bir dosyaya yazarsın.
Ders 28'deki sampiyon-api projene dokunmuyoruz. Bu ders için yeni ve kısa bir proje kur: kalici-api. Kodu kısa tutuyoruz ki dikkatin dosyalarda ve ayarlarda kalsın; öğrendiklerini sonra Ders 28'deki API'ne taşıyabilirsin.
PS C:\kod> mkdir kalici-apiPS C:\kod> cd kalici-apiPS C:\kod\kalici-api> npm init -y# mkdir'in yazdığı tabloyu ve npm init'in yazdığı package.json'u geçebilirsinPS C:\kod\kalici-api> npm install express added 65 packages, and audited 66 packages in 1s 28 packages are looking for funding run `npm fund` for details found 0 vulnerabilitiesPaket sayıları sende biraz farklı çıkabilir. Klasörü VS Code'da aç (File → Open Folder) ve Ders 28'deki gibi hemen .gitignore adında bir dosya oluşturup içine node_modules/ yaz; bu dosyayı ders boyunca büyüteceksin. Sonra package.json'da iki şeyi değiştir: "type": "commonjs" satırını "type": "module" yap (Ders 14), "scripts" kısmını da şöyle yaz:
"scripts": { "start": "node sunucu.js", "dev": "node --watch sunucu.js" },npm run dev sunucuyu Ders 27'deki gibi --watch ile başlatacak. Bu dersteki bütün dosyalar C:\kod\kalici-api klasöründe.
node:fs/promises: dosya ve klasör
Ders 17'de oyunun kaydını readFile ve writeFile ile tek bir dosyaya yazmıştın. Gerçek programlar çoğu zaman klasörlerle de çalışır: her maç için ayrı bir dosya, bir kayıt klasörü, yedekler. dosya-deneme.js adında bir dosya aç ve yaz:
import { mkdir, writeFile, readdir, readFile } from "node:fs/promises" await mkdir("maclar", { recursive: true })await writeFile("maclar/mac-1.json", JSON.stringify({ sampiyon: "Ahri", sonuc: "zafer" }), "utf8")await writeFile("maclar/mac-2.json", JSON.stringify({ sampiyon: "Garen", sonuc: "yenilgi" }), "utf8") const adlar = await readdir("maclar")console.log(adlar) for (const ad of adlar) { const metin = await readFile("maclar/" + ad, "utf8") const mac = JSON.parse(metin) console.log(`${ad}: ${mac.sampiyon}, ${mac.sonuc}`)}[ 'mac-1.json', 'mac-2.json' ]mac-1.json: Ahri, zafermac-2.json: Garen, yenilginode dosya-deneme.js ile çalıştır. VS Code'un dosya listesinde maclar klasörü ve içinde iki dosya belirir. Programı ikinci kez çalıştır: aynı çıktı gelir, hata çıkmaz. Kullandığın dört fonksiyon ve birazdan işine yarayacak beşincisi:
| Fonksiyon | Ne yapar | Dikkat |
|---|---|---|
readFile(dosya, "utf8") | Dosyanın içini metin olarak okur | Dosya yoksa ENOENT hatası |
writeFile(dosya, metin, "utf8") | Metni dosyaya yazar; dosya varsa önce içini boşaltır, sonra yazar | Klasörü yoksa ENOENT hatası |
mkdir(klasor, { recursive: true }) | Klasör açar; iç içe klasörleri de açar | recursive yoksa, klasör zaten varken EEXIST hatası |
readdir(klasor) | Klasördeki dosya ve klasörlerin adlarını dizi olarak verir | Tam yol değil, yalnız ad döner |
rename(eski, yeni) | Dosyanın adını değiştirir, gerekirse taşır | Birazdan çok işine yarayacak |
{ recursive: true } iki iş görür: kayitlar/2026/ekim gibi iç içe klasörlerin hepsini tek seferde açar ve klasör zaten varsa hata vermez. Onu yazmasaydın ikinci çalıştırmada EEXIST: file already exists (dosya zaten var) hatası alırdın. E ile başlayan bu büyük harfli kodlar işletim sisteminden gelir: ENOENT "böyle bir şey yok", EEXIST "zaten var" demek.
readdir sana yalnız adları verir: mac-1.json, mac-2.json. Dosyayı okurken başına klasörü eklemen gerekir; bu yüzden "maclar/" + ad yazdık. Yalnız readFile(ad, "utf8") yazsaydın Node dosyayı maclar'ın içinde değil, terminalin bulunduğu klasörde arar ve ENOENT verirdi. Yolu metin birleştirerek kurmak çalışıyor ama kırılgan; bir sonraki bölümün konusu bu.
Error: EISDIR: illegal operation on a directory, readNe zaman çıkar? Dosya bekleyen bir fonksiyona klasör verince. EISDIR "error: is directory", yani "bu bir klasör" demek.
import { mkdir, readFile } from "node:fs/promises" await mkdir("maclar", { recursive: true })const metin = await readFile("maclar", "utf8")node:internal/fs/promises:1261 const bytesRead = (await PromisePrototypeThen( ^ Error: EISDIR: illegal operation on a directory, read at async readFileHandle (node:internal/fs/promises:1261:24) at async file:///C:/kod/kalici-api/dosya-deneme.js:4:15 { errno: -4068, code: 'EISDIR', syscall: 'read'} Node.js v24.20.0İlk üç satır Node'un kendi içinden geliyor; senin satırın at ile başlayan ikinci satırda: dosya-deneme.js:4. Ders 17'de gördüğün gibi süslü parantezdeki code alanı hatanın türünü söylüyor.
Nasıl düzelir? Yolun sonuna dosyanın adını ekle: readFile("maclar/mac-1.json", "utf8"). Klasörün içindekileri öğrenmek istiyorsan readFile değil readdir kullan.
Error: ENOENT: no such file or directory, open '…\kayitlar\mac-1.json'Ne zaman çıkar? Var olmayan bir klasörün içine dosya yazmaya çalışınca. Ders 17'de aynı hatayı okurken görmüştün; bu kez yazarken çıkıyor.
import { writeFile } from "node:fs/promises" await writeFile("kayitlar/mac-1.json", "{}", "utf8")PS C:\kod\kalici-api> node dosya-deneme.jsnode:internal/fs/promises:1345 return new FileHandle(await PromisePrototypeThen( ^ Error: ENOENT: no such file or directory, open 'C:\kod\kalici-api\kayitlar\mac-1.json' at async open (node:internal/fs/promises:1345:25) at async writeFile (node:internal/fs/promises:1965:14) at async file:///C:/kod/kalici-api/dosya-deneme.js:3:1 { errno: -4058, code: 'ENOENT', syscall: 'open', path: 'C:\\kod\\kalici-api\\kayitlar\\mac-1.json'} Node.js v24.20.0writeFile dosyayı açar ama klasörü açmaz. kayitlar klasörü olmadığı için dosyanın açılacağı yer de yok.
Nasıl düzelir? Yazmadan önce klasörü aç: await mkdir("kayitlar", { recursive: true }). recursive sayesinde bu satırı her çalıştırmada güvenle tekrarlayabilirsin.
Dosya yolları: node:path ve import.meta.dirname
Dosya yolunu bir metin olarak yazıyoruz, ama yollar sıradan metinler değil. Windows klasörleri ters eğik çizgiyle (\) ayırır, internetteki sunucuların çoğunu çalıştıran Linux ise düz eğik çizgiyle (/). Bir de göreli yolların kime göre çözüldüğü sorusu var. Üç tuzağı sırayla görelim.
Ters eğik çizgi tuzağı
Ders 17'de "\n"'in metnin içinde yeni satır demek olduğunu görmüştün. Ters eğik çizgi metnin içinde özel bir işaret başlatır, bu yüzden Windows yolunu tırnak içine olduğu gibi yazmak bozulur. yol-deneme.js adında bir dosya aç:
const yanlis = "C:\kod\kalici-api\notlar.json"const ciftCizgi = "C:\\kod\\kalici-api\\notlar.json"const egikCizgi = "C:/kod/kalici-api/notlar.json"console.log(yanlis)console.log(ciftCizgi)console.log(egikCizgi)C:kodkalici-apiotlar.jsonC:\kod\kalici-api\notlar.jsonC:/kod/kalici-api/notlar.jsonİlk satırda \k sessizce k oldu, \n ise yeni satıra dönüştü; ortada hata mesajı bile yok. İkinci satırda her \ iki kez yazıldı: \\ "gerçekten bir ters eğik çizgi" demek. Üçüncü satır düz eğik çizgi kullanıyor; Node, Windows'ta da /'yi anlar. Kendi kodunda yolları elle birleştirmek yerine bir sonraki bölümdeki aracı kullanacaksın.
path.join: parçaları doğru birleştirmek
Node'un node:path modülü yollarla çalışmak için yazılmış küçük fonksiyonlar verir. yol-deneme.js'in içini sil ve şunu yaz:
import path from "node:path" console.log(path.join("maclar", "mac-1.json"))console.log(path.join("C:/kod/kalici-api", "maclar/", "mac-1.json"))console.log(path.join("maclar", "..", "veri.json"))console.log(path.extname("mac-1.json"))console.log(path.basename("C:\\kod\\kalici-api\\veri.json"))maclar\mac-1.jsonC:\kod\kalici-api\maclar\mac-1.jsonveri.json.jsonveri.jsonpath.join(...)parçaları işletim sisteminin ayırıcısıyla birleştirir. Windows'ta\koydu; aynı kod Linux'tamaclar/mac-1.jsonüretir. İkinci satırda fazladan/'yi temizledi, eğik çizgileri de Windows'unkine çevirdi..."bir üst klasör" demek:maclar'a girip geri çıktı, geriyeveri.jsonkaldı.path.extnameuzantıyı,path.basenameyolun son parçasını, yani dosyanın adını verir.
Kodun bir gün internetteki bir Linux sunucusunda çalışacak. Yolları "maclar\\" + ad diye elle yazarsan orada bozulur; path.join(...) iki yerde de doğru yolu kurar.
Göreli yol kime göre?
Ders 17'nin sonunda bir tuzak görmüştün: oyunu bir üst klasörden başlatınca kayıt dosyası proje klasörüne değil, başka bir yere yazılmıştı. Sebebi, "kayit.json" gibi göreli yolların programın dosyasına göre değil, terminalin o an bulunduğu klasöre göre çözülmesi.
Farkı gözünle gör. nerede.js adında bir dosya aç, yaz ve iki farklı klasörden çalıştır:
import path from "node:path" console.log("Çalışma klasörü:", process.cwd())console.log("Dosyanın klasörü:", import.meta.dirname)console.log("Göreli yol:", path.resolve("veri.json"))console.log("Sabit yol:", path.join(import.meta.dirname, "veri.json"))PS C:\kod\kalici-api> node nerede.jsÇalışma klasörü: C:\kod\kalici-apiDosyanın klasörü: C:\kod\kalici-apiGöreli yol: C:\kod\kalici-api\veri.jsonSabit yol: C:\kod\kalici-api\veri.jsonPS C:\kod\kalici-api> cd ..PS C:\kod> node kalici-api\nerede.jsÇalışma klasörü: C:\kodDosyanın klasörü: C:\kod\kalici-apiGöreli yol: C:\kod\veri.jsonSabit yol: C:\kod\kalici-api\veri.jsonİkinci çalıştırmada göreli yol C:\kod\veri.json'a kaydı; sabit yol yerinden oynamadı. Denemeden sonra cd kalici-api ile proje klasörüne dön.
processNode'un, çalışan programı anlatan hazır objesi.process.cwd()çalışma klasörünü verir.import.meta.dirnamebu kodun yazılı olduğu dosyanın klasörü. Programı nereden başlatırsan başlat değişmez.path.resolve("veri.json")göreli bir yolu çalışma klasörüne göre tam yola çevirir. Çıktıda göreli yolun nereye gittiğini bu yüzden görebildik.
"veri.json" terminalin klasörüne göre çözülür ve yanlış yere gidebilir. import.meta.dirname ile kurulan yol hep dosyanın yanındaki veri.json'u gösterir.Kural: programın kendi dosyalarını (veri, şablon, sayfa klasörü) her zaman path.join(import.meta.dirname, ...) ile bul. Böylece sunucuyu VS Code'un terminalinden de, başka bir klasörden de, canlı sunucudaki bir başlatıcıdan da çalıştırsan aynı dosyaya ulaşır. Ders 28'deki express.static("public") da bu yüzden kırılgandı; birazdan onu da düzelteceksin.
ReferenceError: __dirname is not defined in ES module scopeNe zaman çıkar? İnternetten kopyaladığın eski bir kodda __dirname geçiyorsa. __dirname, CommonJS döneminin import.meta.dirname'iydi; ES modüllerinde yok.
import path from "node:path" const dosya = path.join(__dirname, "veri.json")console.log(dosya)PS C:\kod\kalici-api> node eski-kod.jsfile:///C:/kod/kalici-api/eski-kod.js:3const dosya = path.join(__dirname, "veri.json") ^ ReferenceError: __dirname is not defined in ES module scopeThis file is being treated as an ES module because it has a '.js' file extension and '\\?\C:\kod\kalici-api\package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension. at file:///C:/kod/kalici-api/eski-kod.js:3:25 ... Node.js v24.20.0İkinci satır, dosyanın neden ES modülü sayıldığını açıklıyor: package.json'da "type": "module" yazıyor.
Nasıl düzelir? __dirname yerine import.meta.dirname yaz. Bazı kaynaklarda fileURLToPath(import.meta.url) ile başlayan iki üç satırlık bir kalıp görürsün; import.meta.dirname Node 20.11'den önce yoktu, o kalıp onun eski karşılığı.
JSON dosyası: API'nin küçük veritabanı
Şimdi asıl işe geçiyoruz: API'nin verisini bir dosyada tutmak. Plan sade. Bütün veri veri.json adlı tek bir dosyada duracak; her istek dosyayı okuyacak, gerekiyorsa değiştirip geri yazacak:
{ "sonId": 2, "sampiyonlar": [ { "id": 1, "isim": "Ahri" }, { "id": 2, "isim": "Lux" } ]}sonId'yi Ders 28'den tanıyorsun: verilen son numara. sampiyonlar.length + 1 silmeden sonra aynı numarayı iki kez veriyordu; sonId bunu önlüyordu. Orada bellekteki bir değişkendi ve yeniden başlatınca 3'e dönüyordu. Şimdi verinin yanında, dosyada duruyor; o da kalıcı olacak.
Okuyan ve yazan iki fonksiyon
Dosya işlerini sunucudan ayrı bir modülde topla. depo.js adında bir dosya aç:
import { readFile, writeFile } from "node:fs/promises"import path from "node:path" const DOSYA = path.join(import.meta.dirname, "veri.json") export async function oku() { try { const metin = await readFile(DOSYA, "utf8") return JSON.parse(metin) } catch (hata) { if (hata.code === "ENOENT") { return { sonId: 0, sampiyonlar: [] } } throw hata }} export async function yaz(veri) { await writeFile(DOSYA, JSON.stringify(veri, null, 2), "utf8")}DOSYAbir önceki bölümün kuralına uyuyor: sunucu nereden başlarsa başlasın aynıveri.json.okuDers 17'deki kalıp: dosya yoksa (ENOENT) bu bir hata değil, "henüz veri yok" demek; boş bir başlangıç verisi döner. Başka bir hatayı ne yapacağını bilmediği için yeniden fırlatır.yazobjeyi okunaklı JSON'a çevirip dosyanın üstüne yazar.
Şimdi sunucu.js'i yaz. Ders 28'deki API'nin iki uç noktası yeter. app.listen'ı Ders 28'deki gibi hata parametresiyle yazıyoruz:
import express from "express"import { oku, yaz } from "./depo.js" const app = express()const port = 3000 app.use(express.json()) app.get("/api/sampiyonlar", async (req, res) => { const veri = await oku() res.json(veri.sampiyonlar)}) app.post("/api/sampiyonlar", async (req, res) => { const isim = req.body?.isim if (typeof isim !== "string" || isim.trim() === "") { return res.status(400).json({ hata: "isim alanı gerekli" }) } const veri = await oku() veri.sonId = veri.sonId + 1 const yeni = { id: veri.sonId, isim: isim.trim() } veri.sampiyonlar.push(yeni) await yaz(veri) res.status(201).json(yeni)}) app.listen(port, (hata) => { if (hata) { throw hata } console.log(`Sunucu çalışıyor: http://localhost:${port}`)})PS C:\kod\kalici-api> npm run dev > kalici-api@1.0.0 dev> node --watch sunucu.js Sunucu çalışıyor: http://localhost:3000POST route'u üç adımda çalışıyor: dosyayı oku, bellekteki kopyayı değiştir, dosyaya geri yaz. Dene: ikinci bir terminal aç, ahri.json ({ "isim": "Ahri" }) ve lux.json ({ "isim": "Lux" }) dosyalarını oluştur ve gönder:
PS C:\kod\kalici-api> curl.exe http://localhost:3000/api/sampiyonlar[]PS C:\kod\kalici-api> curl.exe --json "@ahri.json" http://localhost:3000/api/sampiyonlar{"id":1,"isim":"Ahri"}PS C:\kod\kalici-api> curl.exe --json "@lux.json" http://localhost:3000/api/sampiyonlar{"id":2,"isim":"Lux"}İlk istekte dosya yoktu; oku boş listeyi döndürdü. İlk POST'tan sonra VS Code'un dosya listesinde veri.json belirdi; ikinci POST'tan sonra içi bölümün başındaki gibi oldu (yalnız JSON.stringify her alanı ayrı satıra yazar). Şimdi asıl denemeyi yap: birinci terminalde Ctrl+C ile sunucuyu durdur, npm run dev ile yeniden başlat ve listeyi iste:
PS C:\kod\kalici-api> curl.exe http://localhost:3000/api/sampiyonlar[{"id":1,"isim":"Ahri"},{"id":2,"isim":"Lux"}]Ahri ve Lux yerinde. Sunucu yeniden başladı ama veri diskte bekliyordu. API'n artık kalıcı. Yine de bu hâliyle iki ciddi tehlikesi var; ikisini de kendin tetikleyeceksin.
Birinci tehlike: yarım kalan dosya
Tablodaki notu hatırla: writeFile dosyanın içini önce boşaltır, sonra yeni metni parça parça yazar. Bu iki an arasında program ölürse ne olur? Elektrik kesilebilir, bilgisayar donabilir, sunucu bir hatayla çökebilir. Bunu taklit eden küçük bir program yaz. Önce verinin yedeğini al, çünkü bu deneme onu silecek. PowerShell'in Copy-Item kaynak hedef komutu bir dosyayı kopyalar:
PS C:\kod\kalici-api> Copy-Item veri.json veri-yedek.jsoncokme.js adında bir dosya aç:
import { yaz } from "./depo.js" const sampiyonlar = []for (let i = 1; i <= 200000; i++) { sampiyonlar.push({ id: i, isim: "Şampiyon " + i })} yaz({ sonId: 200000, sampiyonlar: sampiyonlar })setTimeout(() => { console.log("Elektrik kesildi!") process.exit(1)}, 3)Program 200.000 şampiyonluk büyük bir veriyi yazmaya başlıyor ve 3 milisaniye sonra kendini kapatıyor. process.exit(1) programı o anda, yarım işlerini bitirmeden durdurur; kesintiyi böyle taklit ediyoruz. yaz'ın önüne bilerek await koymadık: yazma sürerken saatin işlemesini istiyoruz. Sunucu çalışırken ikinci terminalde dene:
PS C:\kod\kalici-api> node cokme.jsElektrik kesildi!PS C:\kod\kalici-api> (Get-Item veri.json).Length0PS C:\kod\kalici-api> curl.exe http://localhost:3000/api/sampiyonlar<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Error</title></head><body><pre>SyntaxError: Unexpected end of JSON input<br> at JSON.parse (<anonymous>)<br> at oku (file:///C:/kod/kalici-api/depo.js:9:21)<br> at async file:///C:/kod/kalici-api/sunucu.js:10:18</pre></body></html>(Get-Item veri.json).Length dosyanın bayt cinsinden boyutunu verir: 0. Yeni veri yazılamadan program öldü, eski veri ise çoktan silinmişti; Ahri ve Lux gitti. Sende boyut 0 yerine yarım bir sayı da çıkabilir; hangi anda kesildiğine bağlı. API artık her istekte 500 dönüyor ve birinci terminalde aynı hata yazıyor:
SyntaxError: Unexpected end of JSON input at JSON.parse (<anonymous>) at oku (file:///C:/kod/kalici-api/depo.js:9:21) at async file:///C:/kod/kalici-api/sunucu.js:10:18Express hatayı yakaladı, sunucu çökmedi; ama dosya bozuk kaldıkça hiçbir istek başarılı olamaz. Yanıttaki sayfanın yığın izini, dosya yollarını ve satır numaralarını herkese göstermesi Express'in hazır hata sayfasından; bunu dersin sonunda, canlı ortam bölümünde yeniden ele alacağız. Şimdilik yedeği geri koy:
PS C:\kod\kalici-api> Copy-Item veri-yedek.json veri.jsonSyntaxError: Unexpected end of JSON inputNe zaman çıkar? JSON.parse'a boş bir metin verince: içi boşalmış bir dosya, yarıda kesilmiş bir yazma, elle boşaltılmış bir kayıt. "JSON girdisi beklenmedik yerde bitti" demek.
import { writeFile, readFile } from "node:fs/promises" await writeFile("bos.json", "", "utf8")const metin = await readFile("bos.json", "utf8")console.log("Okunan karakter sayısı:", metin.length)const veri = JSON.parse(metin)Okunan karakter sayısı: 0<anonymous_script>:1 SyntaxError: Unexpected end of JSON input at JSON.parse (<anonymous>) at file:///C:/kod/kalici-api/bos.js:6:19 Node.js v24.20.0Dosya vardı, okundu, ama içi boştu. readFile hata vermedi; hata bir satır sonra, metni çözmeye çalışınca çıktı.
Nasıl düzelir? Önce sebebi bul: dosyayı kim, ne zaman boşalttı? Yedeğin varsa geri koy. Kalıcı çözüm, dosyanın hiçbir zaman yarım kalmayacağı bir yazma yöntemi: hemen aşağıda.
Geçici dosya ve rename
Çözümün fikri şu: asıl dosyaya hiç dokunmadan yeni veriyi başka bir dosyaya yaz, yazma bitince adını değiştir. Yeniden adlandırmak aynı diskte tek hamlede olur: bir an önce veri.json eski veriyi gösterir, bir an sonra yenisini. Arada yarım bir hâli yoktur.
depo.js'te iki yeri değiştir: ilk satırdaki import'a rename'i ekle ve yaz'ı yeniden yaz:
import { readFile, writeFile, rename } from "node:fs/promises"import path from "node:path" const DOSYA = path.join(import.meta.dirname, "veri.json") export async function oku() { try { const metin = await readFile(DOSYA, "utf8") return JSON.parse(metin) } catch (hata) { if (hata.code === "ENOENT") { return { sonId: 0, sampiyonlar: [] } } throw hata }} export async function yaz(veri) { const gecici = DOSYA + ".tmp" await writeFile(gecici, JSON.stringify(veri, null, 2), "utf8") await rename(gecici, DOSYA)}Yeni veri önce veri.json.tmp'ye yazılıyor. Yazma bütünüyle bitince rename onu veri.json'un yerine koyuyor; eski dosya o anda yenisiyle değişiyor. Kesinti yazma sırasında olursa yarım kalan şey .tmp dosyasıdır, asıl veri sağlam kalır. Kaydet (--watch sunucuyu yeniden başlatır, çünkü depo.js onun içe aktardığı bir dosya) ve aynı çöküşü yeniden dene:
PS C:\kod\kalici-api> node cokme.jsElektrik kesildi!PS C:\kod\kalici-api> (Get-Item veri.json).Length136PS C:\kod\kalici-api> curl.exe http://localhost:3000/api/sampiyonlar[{"id":1,"isim":"Ahri"},{"id":2,"isim":"Lux"}]Program yine yarıda öldü, ama veri.json 136 baytlık eski hâliyle yerinde; API çalışmayı sürdürüyor. Klasörde içi boş ya da yarım bir veri.json.tmp kalmış olabilir. Zararsızdır, bir sonraki başarılı yazma onu ezer; Git'e girmemesini dersin ilerisinde ayarlayacağız.
rename ile değiştirmede asıl dosya ya eski ya yeni hâlindedir, hiçbir zaman yarım değil.İkinci tehlike: aynı anda gelen istekler
Bir sunucu istekleri sırayla değil, gelir gelmez karşılar. İki istek aynı anda gelirse ikisi de dosyayı okur, ikisi de kendi kopyasını değiştirir, ikisi de yazar. Bunu da taklit et: kalabalik.js, on şampiyonu aynı anda eklemeye çalışan bir program. Ders 15'teki Promise.all istekleri aynı anda başlatır, Ders 26'daki ayar objesi de POST gönderir:
const adres = "http://localhost:3000/api/sampiyonlar"const isimler = ["Jinx", "Garen", "Teemo", "Annie", "Yasuo", "Zed", "Vi", "Sona", "Thresh", "Brand"] function ekle(isim) { return fetch(adres, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isim: isim }) })} const yanitlar = await Promise.all(isimler.map(ekle))console.log("Durum kodları:", yanitlar.map((y) => y.status).join(" ")) const listeYaniti = await fetch(adres)const liste = await listeYaniti.json()console.log("Listede", liste.length, "şampiyon var")Listede Ahri ve Lux var; on ekleme başarılı olursa 12 olmalı:
PS C:\kod\kalici-api> node kalabalik.jsDurum kodları: 201 201 201 201 500 500 500 500 500 500Listede 3 şampiyon varOn istekten dördü 201 Created aldı, yani dört kişiye "eklendi" denildi; ama listede yalnız bir yeni şampiyon var. Altısı ise 500 aldı. Sende sayılar her çalıştırmada değişir; bazen son satır bile hata verir, çünkü liste isteği de o anda kaybolmuş ya da bozulmuş bir dosyaya çarpabilir. Sunucunun terminali neler olduğunu söylüyor:
Error: EPERM: operation not permitted, rename 'C:\kod\kalici-api\veri.json.tmp' -> 'C:\kod\kalici-api\veri.json' at async rename (node:internal/fs/promises:1490:10) at async yaz (file:///C:/kod/kalici-api/depo.js:21:5) at async file:///C:/kod/kalici-api/sunucu.js:23:5 { errno: -4048, code: 'EPERM', syscall: 'rename', path: 'C:\\kod\\kalici-api\\veri.json.tmp', dest: 'C:\\kod\\kalici-api\\veri.json'}İki ayrı sorun üst üste biniyor:
- Ezilen değişiklik: İstekler dosyayı neredeyse aynı anda okudu ve hepsi aynı eski listeyi gördü. Her biri listeye kendi şampiyonunu ekleyip dosyaya yazdı; sonra yazan, öncekinin eklediğini görmeden üstüne yazdı. 201 alan dört şampiyondan üçü böyle kayboldu, hem de hiçbir hata mesajı olmadan.
- Aynı geçici dosya: Bütün istekler aynı
veri.json.tmp'ye yazıp onu taşımaya çalıştı. Biri taşırken öteki aynı dosyaya yazıyordu; Windows izin vermedi (EPERM). Bazen de dosya çoktan taşınmış olduğu içinENOENTçıktı.
Böyle bir hata, sen tek başına denerken neredeyse hiç görünmez: istekler sırayla gelir, her şey çalışır. Gerçek kullanıcılar gelince ortaya çıkar. Denemeden sonra yedeği geri koy: Copy-Item veri-yedek.json veri.json.
Yazmaları sıraya koymak
Çözüm, "oku, değiştir, yaz" işini bir anda tek bir isteğin yapması. Diğerleri sırada bekler. depo.js'in sonuna şunu ekle:
let sira = Promise.resolve() export function degistir(islem) { const sonuc = sira.then(async () => { const veri = await oku() const cevap = islem(veri) await yaz(veri) return cevap }) sira = sonuc.catch(() => {}) return sonuc}sira, kuyruğun sonunu tutan bir Promise. BaştaPromise.resolve(): hemen tamamlanmış, "kuyruk boş" demek.- Her
degistirçağrısı kendi işinisira.then(...)ile kuyruğun sonuna ekler.thenöncekinin bitmesini bekler; böylece işler aynı anda değil, birbiri ardına çalışır. - İş, verilen
islemfonksiyonuna veriyi verir, onun döndürdüğünü (cevap) saklar, dosyayı yazar ve cevabı döndürür. sira = sonuc.catch(() => {}): kuyruğun yeni sonu bu iş olur.catchboş bir fonksiyonla hatayı yutar ki bir iş hata verirse sıradakiler sonsuza dek takılmasın. Hatanın kendisi kaybolmaz:return sonuconudegistir'i çağıran route'a ulaştırır.
sunucu.js'te ikinci satırı import { oku, degistir } from "./depo.js" yap ve POST route'unun son kısmını değiştir:
app.post("/api/sampiyonlar", async (req, res) => { const isim = req.body?.isim if (typeof isim !== "string" || isim.trim() === "") { return res.status(400).json({ hata: "isim alanı gerekli" }) } const yeni = await degistir((veri) => { veri.sonId = veri.sonId + 1 const sampiyon = { id: veri.sonId, isim: isim.trim() } veri.sampiyonlar.push(sampiyon) return sampiyon }) res.status(201).json(yeni)})Route artık dosyaya kendisi dokunmuyor; "listeye şunu ekle" diyen küçük bir fonksiyonu kuyruğa veriyor. Kaydet ve kalabalığı yeniden gönder:
PS C:\kod\kalici-api> node kalabalik.jsDurum kodları: 201 201 201 201 201 201 201 201 201 201Listede 12 şampiyon varOn istek, on ekleme, eksiksiz. Kaç kez çalıştırırsan çalıştır sonuç aynı; yalnız şampiyonların sırası, isteklerin hangisinin önce vardığına göre değişebilir. Listeyi kısa tutmak için yedeği yine geri koy.
Ayarlar koddan çıksın: ortam değişkenleri
sunucu.js'e bir daha bak: const port = 3000 kodun içinde yazıyor. Kendi bilgisayarında sorun yok; ama Ders 32'de API'ni bir barındırıcıya koyduğunda hangi portu dinleyeceğine barındırıcı karar verecek. Birazdan bir de şifre gerekecek: bütün listeyi silen tehlikeli bir route'u yalnız yöneticinin çağırabilmesi için bir anahtar. O anahtarı koda yazarsan kodla birlikte Git'e, GitHub'a ve kodu gören herkese gider.
Çözüm, kodu her yerde aynı bırakıp değişen değerleri programa dışarıdan vermek. Program başlarken işletim sistemi ona bir ayar listesi verir; bu listedeki her ada ortam değişkeni denir.
process objesini çalışma klasörü bölümünden tanıyorsun. process.env onun içinde; programın gördüğü bütün ortam değişkenlerini tutan bir obje. ortam.js adında bir dosya aç:
console.log("İşletim sistemi:", process.env.OS)console.log("Port:", process.env.PORT)console.log("Anahtar:", process.env.YONETICI_ANAHTARI)İşletim sistemi: Windows_NTPort: undefinedAnahtar: undefinedOS Windows'un kendi koyduğu bir değişken; her Windows'ta Windows_NT'dir. PORT ve YONETICI_ANAHTARI'nı henüz kimse tanımlamadı; objede olmayan bir anahtar gibi undefined döndü (Ders 4). PowerShell'de bir ortam değişkenini $env:AD = "değer" ile tanımlarsın. Aynı terminalde dene:
PS C:\kod\kalici-api> $env:PORT = "4000"PS C:\kod\kalici-api> node ortam.jsİşletim sistemi: Windows_NTPort: 4000Anahtar: undefinedKodu değiştirmedin, çıktı değişti. $env: ile tanımlanan değişken yalnız o terminalde ve oradan başlatılan programlarda yaşar. Terminali kapatınca gider; VS Code'da açtığın başka bir terminal onu hiç görmez. Denemeden sonra bu terminali kapat (terminal listesindeki çöp kutusu simgesi) ve yenisini aç ki PORT sonraki denemelere karışmasın.
TypeError: Cannot read properties of undefined (reading 'slice')Ne zaman çıkar? Tanımlanmamış bir ortam değişkenini metin sanıp kullanınca. process.env.X yoksa hata vermez, sessizce undefined döner; hata ancak değeri kullanırken çıkar.
const anahtar = process.env.YONETICI_ANAHTARIconsole.log("Anahtarın ilk 4 harfi:", anahtar.slice(0, 4))file:///C:/kod/kalici-api/anahtar.js:2console.log("Anahtarın ilk 4 harfi:", anahtar.slice(0, 4)) ^ TypeError: Cannot read properties of undefined (reading 'slice') at file:///C:/kod/kalici-api/anahtar.js:2:47 ... Node.js v24.20.0Nasıl düzelir? Önce değişken gerçekten tanımlı mı bak: console.log(process.env.YONETICI_ANAHTARI). undefined görüyorsan ya hiç tanımlamadın ya adını farklı yazdın ya da programı .env dosyasını yüklemeden başlattın (birazdan göreceksin). Kalıcı çözüm, eksik ayarı program açılırken yakalayıp açık bir mesajla durmak; bunu ayarlar.js'te yapacaksın.
.env dosyası ve node --env-file
Her seferinde $env: yazmak yorucu; terminali kapatınca da her şey gidiyor. Bir projede genelde beş on ayar olur. Çözüm, ayarları proje klasöründe .env adlı bir metin dosyasına yazmak ve Node'a, programı başlatırken o dosyayı okutmak.
.env birazdan gerçek bir anahtar taşıyacak. Git'e bir an bile girmemesi için sırayı bozma: önce .gitignore, sonra .env. Kurulumda açtığın .gitignore'u şu hâle getir:
node_modules/.envveri*.json*.tmp.env: birazdan açacağın ayar dosyası.veri*.json:veri.jsonveveri-yedek.json;*"ne olursa olsun" demek (Ders 7). Veri dosyası kodun parçası değil: her istekte değişir ve gerçek bir uygulamada kullanıcıların bilgilerini taşır.*.tmp: yarıda kalmış geçici dosyalar.
Şimdi VS Code'da yeni bir dosya aç, adını tam olarak .env koy (başında nokta var, uzantısı yok), içine iki satır yaz ve ortam.js'i bu kez --env-file seçeneğiyle çalıştır:
PORT=3000YONETICI_ANAHTARI=gelistirme-anahtari-42PS C:\kod\kalici-api> node --env-file=.env ortam.jsİşletim sistemi: Windows_NTPort: 3000Anahtar: gelistirme-anahtari-42Her satır bir değişken: ad, =, değer. Adlar geleneksel olarak büyük harf ve alt çizgiyle yazılır. Listeye sığsın diye node_modules klasörünü ve önceki bölümlerin deneme dosyalarını göstermedik.
--env-file=.env Node'a "programı başlatmadan önce bu dosyadaki satırları process.env'e koy" der. Node 20.6'dan beri Node'un içinde, Node 24.10'dan beri de deneysel değil. İnternetteki eski örneklerde aynı işi gören dotenv paketini (import "dotenv/config") görürsün; Node 24'te ona gerek yok. Bir kural daha: terminalde aynı adla bir değişken zaten tanımlıysa ($env:PORT gibi) o kazanır; Node .env'deki değeri ortamda zaten olan bir değerin üstüne yazmaz.
.env'in yazım kuralları
Node .env'i kendi kurallarıyla okur ve bazı kurallar seni şaşırtabilir. Hepsini bir kerede gör: VS Code'da kurallar.env adında bir dosya aç ve yaz:
# Bu satır yorum, Node okumazBOSLUKLU = deneme-123TIRNAKSIZ=Sihirdar VadisiTIRNAKLI="Sihirdar Vadisi"NOTLU=3000 # geliştirme portuKARE=ab#cdTIRNAKTA_KARE="ab#cd"NOKTALI_VIRGUL=3000;KIVRIK=“abc”BOS=goster.js her değeri JSON.stringify ile yazdırıyor (Ders 12); böylece tırnakları, boş metni ve undefined'ı birbirinden ayırt edebilirsin:
const adlar = ["BOSLUKLU", "TIRNAKSIZ", "TIRNAKLI", "NOTLU", "KARE", "TIRNAKTA_KARE", "NOKTALI_VIRGUL", "KIVRIK", "BOS", "YOK"]for (const ad of adlar) { console.log(ad, JSON.stringify(process.env[ad]))}PS C:\kod\kalici-api> node --env-file=kurallar.env goster.jsBOSLUKLU "deneme-123"TIRNAKSIZ "Sihirdar Vadisi"TIRNAKLI "Sihirdar Vadisi"NOTLU "3000"KARE "ab"TIRNAKTA_KARE "ab#cd"NOKTALI_VIRGUL "3000;"KIVRIK "“abc”"BOS ""YOK undefined=çevresindeki boşluklar atılır. Tırnaksız değerin içinde boşluk olabilir.- Değeri saran düz tırnaklar (
"ya da') atılır. #'den sonrası yorumdur, değerin ortasında bile:KARE=ab#cdyalnızabverdi. İçinde#geçen bir şifreyi mutlaka tırnak içine al.- Sondaki
;değerin parçasıdır. JavaScript alışkanlığıyla satır sonuna noktalı virgül koyma. - Word ya da WhatsApp'tan gelen kıvrık tırnaklar
“ ”tırnak sayılmaz, değerin içinde kalır (Ders 1'deki akıllı tırnak tuzağı). BOS=boş metin ("") verir,undefineddeğil. Dosyada hiç olmayan değişkenundefinedolur.
Denemeden sonra kurallar.env'i silebilirsin.
.env.example: şablon Git'e girer
.env Git'e girmiyor. Peki projeyi GitHub'dan indiren bir arkadaşın (ya da yeni bilgisayarındaki sen) hangi ayarların gerektiğini nereden bilecek? Cevap, değerleri boş bırakılmış bir şablon. .env.example adında bir dosya aç:
# Bu dosyayı kopyalayıp adını .env yap, değerleri doldur.PORT=3000YONETICI_ANAHTARI=Bu dosya Git'e girer; içinde gizli bir şey yok, yalnız ayarların adları ve zararsız varsayılanlar. Projeyi yeni kuran kişi şablonu kopyalar ve boşlukları kendi değerleriyle doldurur:
PS C:\kod\kalici-api> Copy-Item .env.example .envBu komutu şimdi çalıştırma: senin .env'in zaten var ve Copy-Item hiçbir şey sormadan onun üstüne yazar, anahtarın silinir. Komut, projeyi yeni indiren biri içindir.
.gitignore'daki .env satırı .env.example'ı etkilemez: kural yalnız adı tam olarak .env olan dosyaya uyar.
Ayarları tek yerde toplamak: ayarlar.js
Ayarları kodun her yerinden process.env.X diye okumak yerine tek bir modülde topla. Böylece programın hangi ayarlara ihtiyaç duyduğu tek dosyada görünür ve eksik bir ayar ilk istek geldiğinde değil, program açılırken yakalanır. ayarlar.js adında bir dosya aç:
function gerekli(ad) { const deger = process.env[ad] if (!deger) { throw new Error(`${ad} ayarı yok. .env dosyasına bak.`) } return deger} export const ayarlar = { port: process.env.PORT ?? 3000, yoneticiAnahtari: gerekli("YONETICI_ANAHTARI"), canli: process.env.NODE_ENV === "production"}gerekli(ad), değişkeniprocess.env[ad]diye köşeli parantezle okur, çünkü ad bir değişkenin içinde (Ders 4). Değer yoksa ya da boş metinse (!deger, Ders 12) açık bir mesajla hata fırlatır (Ders 13).port:PORTyoksa 3000 (Ders 12'deki??). Port gizli değil ve olmadığında makul bir varsayılanı var.yoneticiAnahtari: gizli bir değerin varsayılanı olamaz. Yoksa program hiç açılmasın.canli:NODE_ENVadlı değişken"production"isetrue. Bu kalıbı dersin sonunda kullanacaksın.
Sunucu Ders 28'deki gibi bir de sayfa sunsun. public adında bir klasör aç ve içine kısa bir sayfa koy:
<!doctype html><html lang="tr"><head> <meta charset="utf-8"> <title>Şampiyon API'si</title></head><body> <h1>Şampiyon API'si</h1> <p>Liste: <a href="/api/sampiyonlar">/api/sampiyonlar</a></p></body></html>Şimdi sunucu.js'i ayarları kullanacak hâle getir. Değişen yerler vurgulu: yeni import'lar, sabit yollu express.static, yönetici ara katmanı, listeyi silen route ve app.listen:
import express from "express"import path from "node:path"import { ayarlar } from "./ayarlar.js"import { oku, degistir } from "./depo.js" const app = express() app.use(express.json())app.use(express.static(path.join(import.meta.dirname, "public"))) function yoneticiMi(req, res, next) { if (req.get("Authorization") !== `Bearer ${ayarlar.yoneticiAnahtari}`) { return res.status(401).json({ hata: "Yönetici anahtarı eksik ya da yanlış" }) } next()} app.get("/api/sampiyonlar", async (req, res) => { const veri = await oku() res.json(veri.sampiyonlar)}) app.post("/api/sampiyonlar", async (req, res) => { const isim = req.body?.isim if (typeof isim !== "string" || isim.trim() === "") { return res.status(400).json({ hata: "isim alanı gerekli" }) } const yeni = await degistir((veri) => { veri.sonId = veri.sonId + 1 const sampiyon = { id: veri.sonId, isim: isim.trim() } veri.sampiyonlar.push(sampiyon) return sampiyon }) res.status(201).json(yeni)}) app.delete("/api/sampiyonlar", yoneticiMi, async (req, res) => { await degistir((veri) => { veri.sampiyonlar = [] }) res.status(204).end()}) app.listen(ayarlar.port, (hata) => { if (hata) { throw hata } const mod = ayarlar.canli ? "canlı" : "geliştirme" console.log(`Sunucu çalışıyor (${mod}): http://localhost:${ayarlar.port}`)})- 9. satır: Ders 28'deki
express.static("public")artık çalışma klasörüne bağlı değil;publichepsunucu.js'in yanındaki klasör. yoneticiMibir ara katman.req.get("Authorization")isteğinAuthorizationbaşlığını okur; başlık adında büyük-küçük harf fark etmez. Başlık tam olarakBearer+ boşluk + anahtar değilse istek401ile geri döner ve route'a hiç ulaşmaz.app.delete("/api/sampiyonlar", yoneticiMi, ...)bütün listeyi siler.sonId'ye dokunmuyor; silinen numaralar bir daha verilmez.app.listenportu ayarlardan alıyor ve hangi ortamda çalıştığını söylüyor.
sunucu.js'i kaydettiğin anda --watch sunucuyu yeniden başlatır ve birinci terminalde Error: YONETICI_ANAHTARI ayarı yok. .env dosyasına bak. yazıp durur. Şaşırma: dev betiği .env'i henüz okumuyor, ayarlar.js de tam bunu yakaladı. Şimdi package.json'daki dev betiğine --env-file ekle:
"scripts": { "start": "node sunucu.js", "dev": "node --watch --env-file=.env sunucu.js" },start olduğu gibi kaldı; nedenini dersin sonundaki canlı ortam bölümünde göreceksin. --watch package.json'ı izlemez; birinci terminalde sunucuyu Ctrl+C ile durdur ve yeniden başlat:
PS C:\kod\kalici-api> npm run dev > kalici-api@1.0.0 dev> node --watch --env-file=.env sunucu.js Sunucu çalışıyor (geliştirme): http://localhost:3000İkinci terminalden korumalı route'u dene. Bu bir geliştirme anahtarı, komutun içine yazmanın zararı yok; canlıdaki anahtarı ise komutlara yazma (Ders 26):
PS C:\kod\kalici-api> curl.exe -i -X DELETE http://localhost:3000/api/sampiyonlarHTTP/1.1 401 UnauthorizedX-Powered-By: ExpressContent-Type: application/json; charset=utf-8Content-Length: 51ETag: W/"33-JUk/9UMX2vw4/eg1cpYlxj+GxSc"Date: Mon, 12 Oct 2026 00:14:03 GMTConnection: keep-aliveKeep-Alive: timeout=5 {"hata":"Yönetici anahtarı eksik ya da yanlış"}PS C:\kod\kalici-api> curl.exe -X DELETE http://localhost:3000/api/sampiyonlar -H "Authorization: Bearer yanlis"{"hata":"Yönetici anahtarı eksik ya da yanlış"}PS C:\kod\kalici-api> curl.exe -i -X DELETE http://localhost:3000/api/sampiyonlar -H "Authorization: Bearer gelistirme-anahtari-42"HTTP/1.1 204 No ContentX-Powered-By: ExpressDate: Mon, 12 Oct 2026 00:14:03 GMTConnection: keep-aliveKeep-Alive: timeout=5 PS C:\kod\kalici-api> curl.exe http://localhost:3000/api/sampiyonlar[]Anahtarsız ve yanlış anahtarlı istek 401 aldı; doğru anahtarla liste silindi (204, gövdesiz). Listeyi geri getir: Copy-Item veri-yedek.json veri.json. --watch artık .env'i de izliyor. .env'de anahtarın sonundaki 42'yi 43 yapıp kaydet ve sunucunun terminaline bak:
Change detected in 'C:\\kod\\kalici-api\\.env'Restarting 'sunucu.js'Sunucu çalışıyor (geliştirme): http://localhost:3000.env yalnız program açılırken okunur; --watch olmasaydı değişikliği görmek için sunucuyu elle yeniden başlatman gerekirdi. Anahtarı 42'ye geri al.
node: .env: not foundNe zaman çıkar? --env-file ile verdiğin dosya yoksa: adı farklıysa (env, .env.txt), başka bir klasördeyse ya da hiç oluşturulmadıysa. Node programı hiç başlatmaz. Dosya, terminalin çalışma klasörüne göre aranır.
PS C:\kod\kalici-api> npm run dev > kalici-api@1.0.0 dev> node --watch --env-file=.env sunucu.js node: .env: not foundPS C:\kod\kalici-api> node --env-file=.env ortam.jsC:\Program Files\nodejs\node.exe: .env: not foundnpm run dev ile başlatınca satırın başında node, Node'u kendin çalıştırınca tam yolu yazar; mesaj aynı.
Nasıl düzelir? Terminal proje klasöründe mi, istemde C:\kod\kalici-api yazıyor mu, bak. VS Code'un dosya listesinde adın tam olarak .env olduğunu denetle; Not Defteri'yle oluşturduğun dosya, uzantılar gizliyken aslında .env.txt olabilir. Projeyi yeni indirdiysen .env hiç yoktur: Copy-Item .env.example .env ile oluştur ve doldur. Dosya olsa da olmasa da çalışması gereken bir komut için Node'da --env-file-if-exists=.env seçeneği de var; dosya yoksa .env not found. Continuing without it. ("bulunamadı, onsuz devam ediyorum") yazar ve programı yine başlatır.
Error: YONETICI_ANAHTARI ayarı yok. .env dosyasına bak.Ne zaman çıkar? Bu mesajı ayarlar.js'te sen yazdın. .env'de anahtar yoksa, boşsa ya da sunucuyu .env'i okumayan bir komutla başlattıysan çıkar. En sık sebep npm run dev yerine npm start yazmak:
PS C:\kod\kalici-api> npm start > kalici-api@1.0.0 start> node sunucu.js file:///C:/kod/kalici-api/ayarlar.js:4 throw new Error(`${ad} ayarı yok. .env dosyasına bak.`) ^ Error: YONETICI_ANAHTARI ayarı yok. .env dosyasına bak. at gerekli (file:///C:/kod/kalici-api/ayarlar.js:4:15) at file:///C:/kod/kalici-api/ayarlar.js:11:23 ... Node.js v24.20.0Program daha portu açmadan durdu. İyi bir şey: anahtarsız bir sunucunun çalışıp ilk yönetici isteğinde garip davranmasından çok daha kolay anlaşılır.
Nasıl düzelir? Geliştirirken npm run dev kullan. .env'de YONETICI_ANAHTARI= satırının dolu olduğunu, adın harfi harfine aynı yazıldığını denetle.
Error: listen EACCES: permission denied 3000;Ne zaman çıkar? .env'de port satırının sonuna noktalı virgül koyunca: PORT=3000;. Değer "3000;" olur. Node sayıya çevrilemeyen bir metni port numarası saymaz; onu, Windows'ta iki programın konuştuğu adlandırılmış bir hattın (named pipe) adı sanır. 3000; adıyla böyle bir hat açmaya kalkar ve Windows izin vermez. Mesajın portla ilgisi yokmuş gibi görünmesi bu yüzden.
PS C:\kod\kalici-api> npm run dev > kalici-api@1.0.0 dev> node --watch --env-file=.env sunucu.js file:///C:/kod/kalici-api/sunucu.js:46 throw hata ^ Error: listen EACCES: permission denied 3000; at Server.setupListenHandle [as _listen2] (node:net:2302:21) at listenInCluster (node:net:2433:12) at Server.listen (node:net:2575:5) at app.listen (C:\kod\kalici-api\node_modules\express\lib\application.js:605:24) at file:///C:/kod/kalici-api/sunucu.js:44:5 ... { code: 'EACCES', errno: -4092, syscall: 'listen', address: '3000;', port: -1} Node.js v24.20.0Failed running 'sunucu.js'. Waiting for file changes before restarting...Alttaki address: '3000;' satırı ipucunu veriyor: noktalı virgül değerin içinde. Son satır --watch'tan: program çöktü, bir dosya değişene kadar bekliyor.
Nasıl düzelir? .env'deki satırı PORT=3000 yap ve kaydet; --watch sunucuyu kendisi yeniden başlatır.
RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type string ('').Ne zaman çıkar? .env'de PORT= satırı var ama değeri boşsa. process.env.PORT undefined değil, boş metin olur; ?? yalnız null ve undefined'da devreye girdiği için 3000'e geçmez ve app.listen("") çağrılır.
PS C:\kod\kalici-api> npm run dev > kalici-api@1.0.0 dev> node --watch --env-file=.env sunucu.js node:net:2547 validatePort(options.port, 'options.port'); ^ RangeError [ERR_SOCKET_BAD_PORT]: options.port should be >= 0 and < 65536. Received type string (''). at Server.listen (node:net:2547:5) at app.listen (C:\kod\kalici-api\node_modules\express\lib\application.js:605:24) at file:///C:/kod/kalici-api/sunucu.js:44:5 ... { code: 'ERR_SOCKET_BAD_PORT'} Node.js v24.20.0Failed running 'sunucu.js'. Waiting for file changes before restarting...Nasıl düzelir? Ya satıra bir değer yaz (PORT=3000) ya da satırı tamamen sil; satır yoksa ?? 3000'i seçer. Görevlerde boş metni de, saçma değerleri de yakalayan bir port okuyucu yazacaksın.
Gizli bilgiler: koda ve Git'e girmez
.env'in Git'e neden hiç girmemesi gerektiğine yakından bakalım. İçindeki her şey gizli değil: PORT'u herkes bilse zararsız. Ama yönetici anahtarı aynı dosyada duruyor; bu yüzden dosyanın tamamını gizli say.
Dört kural:
- Gizli bilgi koda yazılmaz; program onu
process.env'den okur. .env.gitignore'da durur; Git'e onun yerine.env.examplegirer.- Her commit'ten önce
git status'a bak: listede.envgörüyorsan dur ve.gitignore'u düzelt. - Ekran görüntüsü alırken, yayın açarken, kodunu birine gösterirken
.envaçık bir sekmede durmasın.
Silmek yetmez: Git hatırlar
Kurallara rağmen bir gün .env commit'e girebilir. İlk akla gelen, dosyayı silip yeni bir commit atmak. Bunun neden yetmediğini ayrı bir deneme klasöründe gör. Sunucuyu Ctrl+C ile durdur; birazdan VS Code'da başka bir klasör açacaksın ve terminaller kapanacak. Sonra C:\kod'a geç, sir-deneme adında bir klasör aç, içine gir ve Git'i başlat:
PS C:\kod\kalici-api> cd C:\kodPS C:\kod> mkdir sir-denemePS C:\kod> cd sir-denemePS C:\kod\sir-deneme> git initInitialized empty Git repository in C:/kod/sir-deneme/.git/git init sende hint: ile başlayan satırlar da yazarsa Ders 18'deki gibi geçebilirsin; varsayılan dal adıyla ilgili bir öneri. VS Code'da File → Open Folder ile bu klasörü aç, yeni bir terminal aç (Terminal → New Terminal) ve iki dosya oluştur: içinde console.log("Sunucu") yazan sunucu.js ve içinde YONETICI_ANAHTARI=c6b3876c-4724-44d6-8f6b-9c56a3dea415 yazan .env. Bu klasörde .gitignore yok; yani hata yapan birinin klasörü. Sonra:
PS C:\kod\sir-deneme> git add .PS C:\kod\sir-deneme> git commit -m "İlk kayıt"[master (root-commit) 9ca960e] İlk kayıt 2 files changed, 2 insertions(+) create mode 100644 .env create mode 100644 sunucu.jscreate mode 100644 .env: anahtar kayda girdi. Şimdi "hatayı düzelt": VS Code'da .env'i sil ve silmeyi kaydet:
PS C:\kod\sir-deneme> git add .PS C:\kod\sir-deneme> git commit -m ".env dosyasını sil"[master c0c4e9c] .env dosyasını sil 1 file changed, 1 deletion(-) delete mode 100644 .envPS C:\kod\sir-deneme> git log --onelinec0c4e9c .env dosyasını sil9ca960e İlk kayıtPS C:\kod\sir-deneme> git show 9ca960e:.envYONETICI_ANAHTARI=c6b3876c-4724-44d6-8f6b-9c56a3dea415git show KOD:dosya bir commit'teki dosyanın o anki içini gösterir. Dosya artık klasörde yok, son commit'te de yok; ama ilk kayıt onu olduğu gibi saklıyor. Commit dosyaların o anki kopyasıdır; silmek yeni bir kayıt ekler, eskisini değiştirmez. Senin commit kodların farklı olacak; git show'a git log --oneline listesinin en altındaki, İlk kayıt satırının kodunu yaz.
Depo GitHub'a gittiyse durum daha kötü: depoyu görebilen herkes eski commit'lere tıklayıp dosyayı okuyabilir ve herkese açık depoları anahtar arayarak sürekli tarayan otomatik programlar var. GitHub bazı tanınmış servislerin anahtar biçimlerini herkese açık depolara gönderilirken yakalayıp engelleyebilir (push protection); ama yalnız tanıdığı biçimleri yakalar, senin uydurduğun bir yönetici anahtarını tanımaz. Git geçmişini yeniden yazan araçlar da var (git filter-repo gibi), ama bir anahtar bir kez dışarı çıktıysa onu kopyalanmış say.
Deneme bitti; sir-deneme klasörünü silebilirsin. VS Code'da File → Open Folder ile C:\kod\kalici-api'ye dön, bir terminal aç ve sunucuyu npm run dev ile yeniden başlat. İkinci terminali de aç; bir sonraki bölümde ikisi de gerekecek.
Sızınca ne yapılır?
Bir anahtarın sızdığını fark ettiğin anda, sırasıyla:
- Anahtarı hemen iptal et, yenisini al. Servisin panelinde anahtarı sil ya da yenile; düğmenin adı çoğu yerde "Revoke", "Regenerate" ya da "Roll" gibidir (arayüz değişmiş olabilir). Kendi yönetici anahtarınsa yeni bir değer üret. Geçmişi temizlemekle vakit kaybetme; önce bu.
- Erişim kayıtlarına bak. Servisin kullanım, kayıt (log) ya da fatura sayfasında sızdığı andan beri tanımadığın istekler, beklenmedik harcamalar var mı? Kendi sunucunsa onun kayıtlarına bak. Bir şey bulursan ne zaman ve ne yapıldığını not et.
- Yeni anahtarı yerine koy. Bilgisayarında
.env'e, canlıda barındırıcının paneline yaz ve programı yeniden başlat;.envyalnız açılışta okunur. - Sızıntının yolunu kapat.
.env.gitignore'da mı? Dosya zaten Git'in takibindeyse.gitignore'a yazmak onu gizlemez (Ders 7); aşağıdaki komutla takipten çıkar ve commit'le.
Örneğin .env'i daha önce commit'lediğin bir projede .gitignore'u yeni oluşturup içine .env yazdıysan:
PS C:\kod\bir-proje> git rm --cached .envrm '.env'PS C:\kod\bir-proje> git status --shortD .env?? .gitignore--cached "yalnız Git'in kaydından sil" demek: .env klasöründe durmaya devam eder, Git ise artık onu izlemez. D silindi, ?? henüz izlenmeyen yeni dosya (burada yeni açılan .gitignore) demek. Geçmişteki kopyası yine durur; bu yüzden 4. adım 1. adımın yerini tutmaz.
Yayına giden klasörde gizli dosya kalmasın
Bir ders de gerçek bir olaydan. Kişisel bir site, bir yayın aracıyla tek komutla internete konuyordu. Aracın yüklediği klasörün içinde, sitenin parçası olmayan başka bir projenin alt klasörü ve onun .env dosyası da duruyordu. .env Git'e hiç girmemişti; .gitignore doğruydu. Ama yayın aracı Git'e değil, klasöre bakıyordu: .env de yüklendi ve adres çubuğuna site-adresi/alt-klasor/.env yazan herkes onu indirebiliyordu. Fark edilince dosya yayından kaldırıldı; böyle bir durumda doğru sıra yukarıdaki adımlardır.
Ders: .gitignore Git'in kuralıdır, yayın araçları her zaman Git'e bakmaz. GitHub Pages (Ders 25) deponu yayınlar; Git'e girmeyen dosya oraya da gitmez. Vercel'in komut satırı aracı gibi araçlar ise bilgisayarındaki klasörü yükler. Vercel'in belgesine göre kendiliğinden atladığı listede .env.local var ama düz .env yok; yüklenmesini istemediğin dosyaları .vercelignore adlı ayrı bir dosyaya yazman gerekir (Ekim 2026'daki belge; liste değişmiş olabilir). Her yayın aracının yok sayma kuralı farklıdır; kullandığın aracın belgesine bak.
Kendi sunucunda dene
Aynı hatayı Express'le yap. Diyelim ki biri, sayfa dosyaları proje klasöründe dursun diye express.static'e klasörün kendisini verdi. sunucu.js'te 9. satırı geçici olarak değiştir:
app.use(express.static(import.meta.dirname))PS C:\kod\kalici-api> curl.exe http://localhost:3000/ayarlar.jsfunction gerekli(ad) { const deger = process.env[ad] if (!deger) { throw new Error(`${ad} ayarı yok. .env dosyasına bak.`) } return deger} export const ayarlar = { port: process.env.PORT ?? 3000, yoneticiAnahtari: gerekli("YONETICI_ANAHTARI"), canli: process.env.NODE_ENV === "production"}PS C:\kod\kalici-api> curl.exe http://localhost:3000/.env<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Error</title></head><body><pre>Cannot GET /.env</pre></body></html>Sunucunun kodu herkese açıldı; /veri.json, /sunucu.js ve /package.json da aynı şekilde iner. Express seni bir kez kurtardı: express.static adı noktayla başlayan dosyaları (.env, .gitignore) varsayılan olarak göndermez. Her araç bu kadar dikkatli değil. Aynı klasörü Ders 18'deki npx serve ile aç: birinci terminalde sunucuyu Ctrl+C ile durdur ve aynı yerde npx serve yaz. 3000 boşaldığı için serve de 3000'i alır; kutudaki adresin http://localhost:3000 olduğuna bak. Sonra ikinci terminalden .env'i iste:
PS C:\kod\kalici-api> curl.exe -i http://localhost:3000/.envHTTP/1.1 200 OKContent-Length: 53Content-Disposition: inline; filename=".env"Accept-Ranges: bytesETag: "5575c6da17169cb20ab81bb8262530deb923a767"Date: Mon, 12 Oct 2026 00:26:42 GMTConnection: keep-aliveKeep-Alive: timeout=5 PORT=3000YONETICI_ANAHTARI=gelistirme-anahtari-42200 OK ve anahtar ekranda (biz serve 14.2.6 ile denedik). Bu, olaydaki sitenin başına gelenin aynısı. Birinci terminalde serve'ü Ctrl+C ile durdur, sunucu.js'te 9. satırı geri al (app.use(express.static(path.join(import.meta.dirname, "public")))) ve sunucuyu npm run dev ile yeniden başlat.
.gitignore diskteki dosyaları Git'ten, public klasörü Git'teki dosyaları internetten ayırır.Kural: yayın klasörüne yalnız yayınlanacak dosyalar girer.
- Sunucu kodu,
.envve veri dosyasıpublic'in dışında durur. İçeride yalnız tarayıcıya gidecekler var: HTML, CSS, tarayıcıda çalışan JavaScript, resimler. - Statik bir siteyi yayınlarken de aynı kural geçerli: yayınlanan klasörde not, yedek, başka bir projenin klasörü durmaz.
- Yayın aracının yok sayma dosyasını bil: depodan yayınlayan araçlarda
.gitignore, klasör yükleyen araçlarda aracın kendi dosyası (.vercelignoregibi). - Yayından sonra kendin dene:
curl.exe -I https://siten/.env404dönmeli. Kendi sunucunda şimdi öyle:
PS C:\kod\kalici-api> curl.exe -I http://localhost:3000/.envHTTP/1.1 404 Not FoundX-Powered-By: ExpressContent-Security-Policy: default-src 'none'X-Content-Type-Options: nosniffContent-Type: text/html; charset=utf-8Content-Length: 144Date: Mon, 12 Oct 2026 00:20:06 GMTConnection: keep-aliveKeep-Alive: timeout=5-I yalnız yanıtın başlıklarını ister; gövdeye bakmadan durum koduna bakmanın kısa yolu.
Geliştirme ve canlı ortam
Şimdiye kadar her şey senin bilgisayarında, yani geliştirme ortamında çalıştı. Ders 32'de aynı kod bir barındırıcıda, canlı ortamda çalışacak. Kod aynı kalır; değişen ayarlardır.
| Geliştirme (bilgisayarın) | Canlı (barındırıcı) | |
|---|---|---|
| Ayarlar nereden gelir? | .env ve --env-file | barındırıcının panelinde girilen ortam değişkenleri |
PORT | 3000 | barındırıcı verir |
NODE_ENV | yok | production |
YONETICI_ANAHTARI | gelistirme-anahtari-42 gibi kolay bir değer | uzun, rastgele, yalnız canlıya özel |
| Veri | deneme verisi | gerçek kullanıcıların verisi |
| Hata sayfası | ayrıntılı, yığın izli | genel bir cümle |
process.env'den okur. Değerleri bilgisayarında .env dosyası, canlıda barındırıcının paneli verir.İki ortamda ayrı anahtar kullanmanın iki faydası var: bilgisayarındaki .env bir gün sızarsa canlı etkilenmez; canlıdaki anahtarı da yalnız gereken kişi bilir. Canlı için tahmin edilemez bir anahtar üretmenin kısa yolu:
PS C:\kod\kalici-api> node -p "crypto.randomUUID()"d155edef-8768-473a-a9b5-a2c8ad7c8332node -p tırnak içindeki kodu çalıştırıp sonucunu yazar. crypto'yu Ders 31'de tanıyacaksın, şimdilik kalıp olarak kullan. Her çalıştırmada başka bir 36 karakterlik değer çıkar; bu anahtarı tahmin etmek pratikte imkânsızdır.
NODE_ENV ve Express
NODE_ENV bir gelenek: Node onu kendisi koymaz, barındırıcılar ve sen koyarsın. Değeri production ise birçok paket kendini canlıya göre ayarlar; Express de öyle. Dersin başındaki bozuk veri.json denemesini hatırla: Express'in hata sayfası SyntaxError'ı, dosya yollarını ve satır numaralarını herkese gösteriyordu. Şimdi canlıyı taklit et. Birinci terminalde sunucuyu Ctrl+C ile durdur, VS Code'da yeni bir terminal aç ve ayarları barındırıcının yerine sen ver:
PS C:\kod\kalici-api> $env:NODE_ENV = "production"PS C:\kod\kalici-api> $env:YONETICI_ANAHTARI = "canli-deneme-anahtari"PS C:\kod\kalici-api> npm start > kalici-api@1.0.0 start> node sunucu.js Sunucu çalışıyor (canlı): http://localhost:3000npm start .env'i okumadı; ayarlar ortamdan geldi, tıpkı barındırıcıda olacağı gibi. "canlı" sözcüğü ayarlar.canli'den geliyor. İkinci terminalde veri dosyasını boşalt ve listeyi iste:
PS C:\kod\kalici-api> Clear-Content veri.jsonPS C:\kod\kalici-api> curl.exe http://localhost:3000/api/sampiyonlar<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Error</title></head><body><pre>Internal Server Error</pre></body></html>PS C:\kod\kalici-api> Copy-Item veri-yedek.json veri.jsonClear-Content dosyanın içini boşaltır. Ziyaretçi yalnız Internal Server Error gördü; ayrıntı, yani SyntaxError: Unexpected end of JSON input ve yığın izi, yine sunucunun terminalinde. Geliştirmede ayrıntı işine yarar, canlıda saldırgana sunucunun haritasını verir (Ders 28'deki kural). Kendi hata yakalayıcını yazdığında aynı ayrımı sen yaparsın: ayarlar.canli ise genel bir cümle, değilse ayrıntı. Kimlik doğrulamada da sık kullanılan bir kalıp bu; Ders 31'de çerezlerin bir ayarını onunla vereceksin.
Bitince sunucuyu durdur ve bu terminali kapat; $env:NODE_ENV ve $env:YONETICI_ANAHTARI onunla birlikte gider. Açık kalırsa aynı terminaldeki npm run dev de kendini canlı sanır, çünkü ortamdaki değer .env'den önce gelir.
Görevler
Görev dosyalarını proje klasöründe gorevler adında bir alt klasöre koy ve proje klasöründen çalıştır: node gorevler\esyalar.js. package.json'daki "type": "module" onlar için de geçerli. Ortamda $env: ile tanımladığın bir değişken kalmasın diye yeni bir terminal aç. Önce kendin dene, takılırsan ipuçlarını sırayla aç.
Eşya klasörü
gorevler\esyalar.js adında bir program yaz. Program kendi yanında esyalar adında bir klasör açsın (klasör zaten varsa hata vermesin) ve içine dört dosya yazsın: kilic.json ({ ad: "Uzun Kılıç", fiyat: 350 }), kalkan.json ({ ad: "Doran Kalkanı", fiyat: 450 }), iksir.json ({ ad: "Can İksiri", fiyat: 50 }) ve içinde düz bir cümle olan not.txt. Sonra klasördeki yalnız .json dosyalarını okuyup her eşyayı ve toplam fiyatı yazdırsın:
Can İksiri: 50 altınDoran Kalkanı: 450 altınUzun Kılıç: 350 altınToplam: 850 altınProgram C:\kod'dan node kalici-api\gorevler\esyalar.js diye başlatılsa bile klasörü esyalar.js'in yanında açmalı.
İpucu 1
Klasörün yolu path.join(import.meta.dirname, "esyalar"). Her dosya yolunu da bu klasörle path.join ederek kur.
İpucu 2
readdir yalnız adları verir ve not.txt de aralarında. path.extname(ad) ".json" değilse o turu continue ile atla (Ders 5).
İpucu 3
Döngüden önce let toplam = 0, her eşyada toplam = toplam + esya.fiyat. Dosyaları Windows alfabetik sırayla verdiği için çıktı iksir, kalkan, kilic sırasıyla gelir.
Çözümü göster
import { mkdir, writeFile, readdir, readFile } from "node:fs/promises"import path from "node:path" const klasor = path.join(import.meta.dirname, "esyalar")await mkdir(klasor, { recursive: true }) await writeFile(path.join(klasor, "kilic.json"), JSON.stringify({ ad: "Uzun Kılıç", fiyat: 350 }), "utf8")await writeFile(path.join(klasor, "kalkan.json"), JSON.stringify({ ad: "Doran Kalkanı", fiyat: 450 }), "utf8")await writeFile(path.join(klasor, "iksir.json"), JSON.stringify({ ad: "Can İksiri", fiyat: 50 }), "utf8")await writeFile(path.join(klasor, "not.txt"), "Bu bir eşya değil", "utf8") let toplam = 0for (const ad of await readdir(klasor)) { if (path.extname(ad) !== ".json") { continue } const metin = await readFile(path.join(klasor, ad), "utf8") const esya = JSON.parse(metin) console.log(`${esya.ad}: ${esya.fiyat} altın`) toplam = toplam + esya.fiyat}console.log(`Toplam: ${toplam} altın`)Can İksiri: 50 altınDoran Kalkanı: 450 altınUzun Kılıç: 350 altınToplam: 850 altınSınır denemeleri: programı ikinci kez çalıştır; recursive sayesinde hata yok, çıktı aynı. C:\kod'dan node kalici-api\gorevler\esyalar.js diye çalıştır; C:\kod\esyalar diye bir klasör açılmamalı. Dört writeFile satırının başına // koyup esyalar klasörünü boşalt: Toplam: 0 altın çıkmalı.
Port metnini sayıya çevir
process.env.PORT'tan gelen değeri denetleyen portOku(metin) fonksiyonunu gorevler\port.js'te yaz:
undefinedya da boş metinse3000döndürsün.- 1 ile 65535 arasında (ikisi dahil) bir tam sayıysa o sayıyı sayı olarak döndürsün.
- Değilse
PORT geçersiz: "..."mesajlı bir hata fırlatsın.
Fonksiyonun altına şu deneme döngüsünü yaz:
const denemeler = [undefined, "", "4000", "1", "65535", "0", "65536", "3000;", "3.5", "abc"]for (const metin of denemeler) { try { console.log(JSON.stringify(metin), "->", portOku(metin)) } catch (hata) { console.log(JSON.stringify(metin), "->", hata.message) }}Beklenen çıktı:
undefined -> 3000"" -> 3000"4000" -> 4000"1" -> 1"65535" -> 65535"0" -> PORT geçersiz: "0""65536" -> PORT geçersiz: "65536""3000;" -> PORT geçersiz: "3000;""3.5" -> PORT geçersiz: "3.5""abc" -> PORT geçersiz: "abc"İpucu 1
Önce boş durumu ele al ve erken dön (Ders 6): metin === undefined || metin === "".
İpucu 2
Number(metin) "3000;" ve "abc" için NaN, "3.5" için 3.5 verir. Number.isInteger(...) ikisini de yakalar (Ders 17).
Çözümü göster
function portOku(metin) { if (metin === undefined || metin === "") { return 3000 } const port = Number(metin) if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`PORT geçersiz: "${metin}"`) } return port} const denemeler = [undefined, "", "4000", "1", "65535", "0", "65536", "3000;", "3.5", "abc"]for (const metin of denemeler) { try { console.log(JSON.stringify(metin), "->", portOku(metin)) } catch (hata) { console.log(JSON.stringify(metin), "->", hata.message) }}undefined -> 3000"" -> 3000"4000" -> 4000"1" -> 1"65535" -> 65535"0" -> PORT geçersiz: "0""65536" -> PORT geçersiz: "65536""3000;" -> PORT geçersiz: "3000;""3.5" -> PORT geçersiz: "3.5""abc" -> PORT geçersiz: "abc"Sınır değerleri 1, 65535 geçti; bir eksiği ve bir fazlası (0, 65536) geçmedi. 0'ı neden reddettik? app.listen(0) hata vermez, işletim sisteminden rastgele boş bir port ister; sen 3000'de ararken sunucu başka bir kapıda açılır. Bu fonksiyonu ayarlar.js'te port: portOku(process.env.PORT) diye kullanırsan hata kartlarındaki iki tuzak da sunucu açılırken anlaşılır bir mesajla yakalanır.
Eksik ayarların hepsini söyle
ayarlar.js'teki gerekli ilk eksik ayarda durur: üç ayar eksikse kullanıcı programı üç kez başlatıp üç ayrı hata okur. gorevler\denetim.js'te ayarlariDenetle(adlar) fonksiyonunu yaz:
adlardizisindeki ayarlardanprocess.env'de olmayan ya da boş olan bütün adları bulsun; en az biri varsaEksik ayar: AD1, AD2mesajlı tek bir hata fırlatsın.NODE_ENV"production"iseYONETICI_ANAHTARIen az 32 karakter olmalı; değilseCanlıda YONETICI_ANAHTARI en az 32 karakter olmalıhatası.- Sorun yoksa
Ayarlar tamamyazsın.
Altına şu denemeleri yaz. Denemek için process.env'e kodun içinden değer verebilirsin; değer yalnız bu programın süresince yaşar:
function dene(adlar) { try { ayarlariDenetle(adlar) } catch (hata) { console.log(hata.message) }} process.env.YONETICI_ANAHTARI = "gelistirme-anahtari-42"process.env.BOS_AYAR = "" dene(["YONETICI_ANAHTARI"])dene(["YONETICI_ANAHTARI", "RIOT_ANAHTARI", "BOS_AYAR"])dene([]) process.env.NODE_ENV = "production"dene(["YONETICI_ANAHTARI"])process.env.YONETICI_ANAHTARI = "abcdefghijklmnopqrstuvwxyz12345"dene(["YONETICI_ANAHTARI"])process.env.YONETICI_ANAHTARI = "abcdefghijklmnopqrstuvwxyz123456"dene(["YONETICI_ANAHTARI"])Son iki anahtar 31 ve 32 karakter. Beklenen çıktı:
Ayarlar tamamEksik ayar: RIOT_ANAHTARI, BOS_AYARAyarlar tamamCanlıda YONETICI_ANAHTARI en az 32 karakter olmalıCanlıda YONETICI_ANAHTARI en az 32 karakter olmalıAyarlar tamamİpucu 1
Eksikleri filter ile bul (Ders 11): adlar.filter((ad) => !process.env[ad]). ! hem undefined'ı hem boş metni yakalar.
İpucu 2
Mesajı "Eksik ayar: " + eksikler.join(", ") ile kur (Ders 17). Boş dizide filter boş dizi döndürür; hata fırlatmamalısın.
İpucu 3
Canlı kuralında anahtar hiç olmayabilir; .length'i undefined'da çağırırsan program çöker. const anahtar = process.env.YONETICI_ANAHTARI ?? "" ile başla.
Çözümü göster
function ayarlariDenetle(adlar) { const eksikler = adlar.filter((ad) => !process.env[ad]) if (eksikler.length > 0) { throw new Error("Eksik ayar: " + eksikler.join(", ")) } const anahtar = process.env.YONETICI_ANAHTARI ?? "" if (process.env.NODE_ENV === "production" && anahtar.length < 32) { throw new Error("Canlıda YONETICI_ANAHTARI en az 32 karakter olmalı") } console.log("Ayarlar tamam")} function dene(adlar) { try { ayarlariDenetle(adlar) } catch (hata) { console.log(hata.message) }} process.env.YONETICI_ANAHTARI = "gelistirme-anahtari-42"process.env.BOS_AYAR = "" dene(["YONETICI_ANAHTARI"])dene(["YONETICI_ANAHTARI", "RIOT_ANAHTARI", "BOS_AYAR"])dene([]) process.env.NODE_ENV = "production"dene(["YONETICI_ANAHTARI"])process.env.YONETICI_ANAHTARI = "abcdefghijklmnopqrstuvwxyz12345"dene(["YONETICI_ANAHTARI"])process.env.YONETICI_ANAHTARI = "abcdefghijklmnopqrstuvwxyz123456"dene(["YONETICI_ANAHTARI"])Ayarlar tamamEksik ayar: RIOT_ANAHTARI, BOS_AYARAyarlar tamamCanlıda YONETICI_ANAHTARI en az 32 karakter olmalıCanlıda YONETICI_ANAHTARI en az 32 karakter olmalıAyarlar tamamBOS_AYAR tanımlı ama boş; yine de eksik sayıldı, çünkü boş bir ayar çoğu zaman unutulmuş bir ayardır. 31 karakter reddedildi, 32 kabul edildi. Geliştirme anahtarı (22 karakter) geliştirmede sorun değil, canlıda programı açtırmıyor: biri .env'i olduğu gibi canlıya taşırsa bu kural onu durdurur. crypto.randomUUID()'nin ürettiği 36 karakterlik anahtar kuralı geçer.
Bozuk kaydı açıkça söyle
TFT'deki ilerlemeni bir dosyada tutan iki fonksiyon yaz (gorevler\kayit.js):
guvenliYaz(dosya, veri): veriyi okunaklı JSON olarak atomik yazsın (geçici dosya +rename).guvenliOku(dosya, varsayilan): dosya yoksavarsayilan'ı döndürsün. Dosya var ama JSON olarak çözülemiyorsatft-kayit.json bozuk, elle bak (...)gibi, dosyanın adını ve asıl hatanın mesajını içeren yeni bir hata fırlatsın. Başka hataları olduğu gibi yeniden fırlatsın.
Dosya yolunu path.join(import.meta.dirname, "tft-kayit.json") ile kur ve şu dört durumu dene: olmayan bir dosyayı oku; { altin: 50, seviye: 7 } yazıp geri oku; dosyayı boş metinle ez ve oku; dosyaya JSON.stringify({ altin: 50, seviye: 7 }).slice(0, 12) (yarıda kesilmiş bir yazma) koyup oku. Varsayılan { altin: 0, seviye: 1 } olsun. Beklenen çıktı:
Dosya yok: { altin: 0, seviye: 1 }Yazıp okudum: { altin: 50, seviye: 7 }Hata: tft-kayit.json bozuk, elle bak (Unexpected end of JSON input)Hata: tft-kayit.json bozuk, elle bak (Expected double-quoted property name in JSON at position 12 (line 1 column 13))İpucu 1
guvenliYaz, depo.js'teki yaz'ın aynısı; tek farkı dosyayı parametre olarak alması.
İpucu 2
guvenliOku'da iki ayrı try kullan: biri readFile için (yalnız ENOENT'te varsayılan), biri JSON.parse için. Metni ilk try'ın dışında kullanabilmek için let metin ile önce tanımla (Ders 10).
İpucu 3
Dosyanın adı için path.basename(dosya), asıl hatanın mesajı için hata.message.
Çözümü göster
import { readFile, writeFile, rename } from "node:fs/promises"import path from "node:path" async function guvenliOku(dosya, varsayilan) { let metin try { metin = await readFile(dosya, "utf8") } catch (hata) { if (hata.code === "ENOENT") { return varsayilan } throw hata } try { return JSON.parse(metin) } catch (hata) { throw new Error(`${path.basename(dosya)} bozuk, elle bak (${hata.message})`) }} async function guvenliYaz(dosya, veri) { const gecici = dosya + ".tmp" await writeFile(gecici, JSON.stringify(veri, null, 2), "utf8") await rename(gecici, dosya)} const yeniOyuncu = { altin: 0, seviye: 1 }const kayit = path.join(import.meta.dirname, "tft-kayit.json") console.log("Dosya yok:", await guvenliOku(path.join(import.meta.dirname, "yok.json"), yeniOyuncu)) await guvenliYaz(kayit, { altin: 50, seviye: 7 })console.log("Yazıp okudum:", await guvenliOku(kayit, yeniOyuncu)) await writeFile(kayit, "", "utf8")try { await guvenliOku(kayit, yeniOyuncu)} catch (hata) { console.log("Hata:", hata.message)} const yarim = JSON.stringify({ altin: 50, seviye: 7 }).slice(0, 12)await writeFile(kayit, yarim, "utf8")try { await guvenliOku(kayit, yeniOyuncu)} catch (hata) { console.log("Hata:", hata.message)}Dosya yok: { altin: 0, seviye: 1 }Yazıp okudum: { altin: 50, seviye: 7 }Hata: tft-kayit.json bozuk, elle bak (Unexpected end of JSON input)Hata: tft-kayit.json bozuk, elle bak (Expected double-quoted property name in JSON at position 12 (line 1 column 13))Neden bozuk dosyada varsayılanı döndürmüyoruz? Çünkü o zaman bir sonraki guvenliYaz sıfırlanmış kaydı diske yazar ve oyuncunun gerçek ilerlemesi kalıcı olarak gider. Yok olan dosya "yeni oyuncu" demek; bozuk dosya ise "burada bir şey ters gitti, dokunmadan önce bak" demek. Mesajdaki dosya adı da, bakılacak yeri gösterir.
Yayın öncesi denetim
Bir klasörü yayına göndermeden önce içinde gizli dosya kalmış mı diye bakan bir program yaz (gorevler\denetle.js). Denemek için program önce kendi yanında yayin adında bir klasör kursun ve içine şu dosyaları yazsın (içerikleri önemsiz, "deneme" olabilir): index.html, stil.css, .env, .env.example, .env.production ve alt klasörde sertifika/sunucu.key. Sonra klasörü alt klasörleriyle birlikte tarasın ve şu kurallara uyan dosyaları listelesin:
- Adı tam olarak
.envolan ya da.env.ile başlayan her dosya tehlikeli;.env.examplehariç. - Uzantısı
.keyya da.pemolan dosyalar (sunucu sertifikalarının gizli anahtarları) tehlikeli.
Beklenen çıktı:
Yayına gitmemesi gereken 3 dosya var:- .env- .env.production- sertifika\sunucu.keyTehlikeli dosya yoksa Temiz: yayına hazır yazmalı.
İpucu 1
readdir'e { recursive: true } verirsen alt klasörlerin içini de listeler; adlar sertifika\sunucu.key gibi yolla gelir. Kuralı uygularken yolun son parçasına, path.basename(yol)'a bak.
İpucu 2
Kuralı tehlikeliMi(ad) adında bir fonksiyona koy. İlk satırda .env.example'ı ele: if (ad === ".env.example") return false. Sonra ad === ".env" || ad.startsWith(".env.") (Ders 9).
İpucu 3
path.extname(".env") boş metin verir; Node, adı noktayla başlayan dosyada o noktayı uzantı saymaz. Bu yüzden .env'i uzantıyla değil, adla yakala. .key ve .pem için ise path.extname doğru araç.
Çözümü göster
import { mkdir, writeFile, readdir } from "node:fs/promises"import path from "node:path" const klasor = path.join(import.meta.dirname, "yayin")await mkdir(path.join(klasor, "sertifika"), { recursive: true })const ornekler = ["index.html", "stil.css", ".env", ".env.example", ".env.production", "sertifika/sunucu.key"]for (const ad of ornekler) { await writeFile(path.join(klasor, ad), "deneme", "utf8")} function tehlikeliMi(ad) { if (ad === ".env.example") { return false } if (ad === ".env" || ad.startsWith(".env.")) { return true } const uzanti = path.extname(ad) return uzanti === ".pem" || uzanti === ".key"} const yollar = await readdir(klasor, { recursive: true })const tehlikeliler = yollar.filter((yol) => tehlikeliMi(path.basename(yol))) if (tehlikeliler.length === 0) { console.log("Temiz: yayına hazır")} else { console.log(`Yayına gitmemesi gereken ${tehlikeliler.length} dosya var:`) for (const yol of tehlikeliler) { console.log("- " + yol) }}Yayına gitmemesi gereken 3 dosya var:- .env- .env.production- sertifika\sunucu.keySınır denemesi: yayin klasörünü sil, ornekler'i ["index.html", "stil.css", ".env.example", "sertifika/okuBeni.txt"] yap ve yeniden çalıştır: Temiz: yayına hazır çıkmalı. .env.example ilk kuralın tam kenarında duruyor: o da .env. ile başlıyor; fonksiyonun ilk satırındaki istisna olmasa haksız yere listelenirdi. Görevi bitirince yayin klasörünü sil: içindeki .env.production gibi dosyaların Git'e girmesini istemezsin.
Tek şampiyonu sil
API'ye tek bir şampiyonu silen bir route ekle: DELETE /api/sampiyonlar/:id.
- Yalnız yönetici anahtarıyla çalışsın (
yoneticiMi). idtam sayı değilse400ve{"hata":"id bir tam sayı olmalı"}.- O numarada şampiyon yoksa
404ve{"hata":"2 numaralı şampiyon yok"}gibi bir mesaj. - Silerse
204, gövdesiz. - Dosyaya
degistirüzerinden yazsın; aynı anda gelen silmeler birbirini ezmesin.
Listede Ahri (1) ve Lux (2) varken beklenen davranış:
PS C:\kod\kalici-api> curl.exe -X DELETE http://localhost:3000/api/sampiyonlar/2{"hata":"Yönetici anahtarı eksik ya da yanlış"}PS C:\kod\kalici-api> curl.exe -i -X DELETE http://localhost:3000/api/sampiyonlar/2 -H "Authorization: Bearer gelistirme-anahtari-42"HTTP/1.1 204 No Content...PS C:\kod\kalici-api> curl.exe -X DELETE http://localhost:3000/api/sampiyonlar/2 -H "Authorization: Bearer gelistirme-anahtari-42"{"hata":"2 numaralı şampiyon yok"}PS C:\kod\kalici-api> curl.exe -X DELETE http://localhost:3000/api/sampiyonlar/abc -H "Authorization: Bearer gelistirme-anahtari-42"{"hata":"id bir tam sayı olmalı"}PS C:\kod\kalici-api> curl.exe http://localhost:3000/api/sampiyonlar[{"id":1,"isim":"Ahri"}]İpucu 1
Sırayı Ders 28'deki sampiyonuBul'dan hatırla: önce Number(req.params.id) ve Number.isInteger, sonra arama. Route'un ikinci parametresi yoneticiMi, üçüncüsü asıl iş.
İpucu 2
degistir'e verdiğin fonksiyon bir şey döndürebilir; degistir onu sana geri verir. Silinip silinmediğini döndür: filtrelemeden önceki uzunluğu sakla, veri.sampiyonlar = veri.sampiyonlar.filter((s) => s.id !== id), sonra iki uzunluğu karşılaştır.
İpucu 3
404 kararını degistir'in dışında ver: içeride yanıt göndermeye kalkma, içerideki fonksiyonun tek işi veriyi değiştirmek.
Çözümü göster
sunucu.js'te listeyi silen route'un altına (app.listen'dan önce) ekle:
app.delete("/api/sampiyonlar/:id", yoneticiMi, async (req, res) => { const id = Number(req.params.id) if (!Number.isInteger(id)) { return res.status(400).json({ hata: "id bir tam sayı olmalı" }) } const silindi = await degistir((veri) => { const onceki = veri.sampiyonlar.length veri.sampiyonlar = veri.sampiyonlar.filter((s) => s.id !== id) return veri.sampiyonlar.length < onceki }) if (!silindi) { return res.status(404).json({ hata: `${id} numaralı şampiyon yok` }) } res.status(204).end()})Yukarıdaki çıktılar bu kodla alındı. Sınır denemeleri: aynı silmeyi ikinci kez yapınca 404; anahtarsız abc isteği 400 değil 401 alır, çünkü yoneticiMi route'tan önce çalışır ve anahtarı olmayana adresin geçerli olup olmadığını bile söylemez. veri.json'a bak: sonId hâlâ 2; sıradaki yeni şampiyon 3 numarayı alır, Lux'ın eski numarası bir daha verilmez. 404 yolunda da dosya yeniden yazılır ama içeriği değişmez; zararsız.
Kendini sına
Soru 1
Bu satır Windows'ta ne yazar?
console.log(path.join("kayitlar", "2026", "..", "ekim.json"))Cevabı göster
kayitlar\ekim.json. .. bir üst klasör demek: 2026'ya girip geri çıktı. path.join parçaları Windows'un ayırıcısı \ ile birleştirir; aynı kod Linux'ta kayitlar/ekim.json yazar.
Soru 2
.env'de YONETICI_ANAHTARI=Gizli#Anahtar-42 yazıyor. process.env.YONETICI_ANAHTARI ne olur? Nasıl düzeltirsin?
Cevabı göster
"Gizli". Node .env'de #'den sonrasını yorum sayar, değerin ortasında bile. Programın beklediği anahtar Gizli#Anahtar-42 olduğu için her yönetici isteği 401 alır ve sebebi hiç görünmez. Düzeltme: değeri tırnak içine al, YONETICI_ANAHTARI="Gizli#Anahtar-42".
Soru 3
Arkadaşın projeni GitHub'dan indirdi, npm install ve npm run dev yazdı, node: .env: not found aldı. Neden? Ne yapmalı?
Cevabı göster
.env .gitignore'da olduğu için GitHub'a hiç gitmedi; arkadaşının klonunda yok, --env-file=.env de bulamıyor. Arkadaşın Copy-Item .env.example .env ile şablonu kopyalamalı ve boş değerleri kendi değerleriyle doldurmalı. Senin gerçek anahtarını ona mesajla göndermek doğru yol değil; kendi geliştirme anahtarını üretsin.
Soru 4
yaz fonksiyonu writeFile(DOSYA, ...) ile doğrudan yazıyor ve tam yazarken elektrik kesiliyor. Ne olabilir? Geçici dosya ve rename bunu nasıl önler?
Cevabı göster
writeFile dosyayı önce boşaltır, sonra yazar. Kesinti arada olursa veri.json boş ya da yarım kalır, eski veri de gitmiştir; sonraki her okuma SyntaxError: Unexpected end of JSON input verir. Geçici dosyayla yeni veri önce veri.json.tmp'ye yazılır; asıl dosyaya ancak yazma bitince, tek hamlelik (atomik) bir rename ile dokunulur. Kesinti hangi anda olursa olsun veri.json ya eski ya yeni hâliyle sağlam kalır.
Soru 5
.env dosyanı yanlışlıkla GitHub'daki herkese açık deponuza gönderdin ve on dakika sonra fark ettin. Dosyayı silip yeni bir commit attın, depoyu da gizli (Private) yaptın. Yeter mi? Ne yaparsın?
Cevabı göster
Yetmez. On dakika, anahtarı arayan otomatik programların onu bulması için yeterli olabilir; silinen dosya da eski commit'te duruyor. Sıra: (1) içindeki her anahtarı hemen iptal et ve yenisini al, (2) servisin kayıtlarında o andan beri tanımadığın kullanım var mı bak, (3) yeni anahtarları .env'e ve canlıdaki panele yaz, (4) .env'i .gitignore'a ekle ve gerekiyorsa git rm --cached .env ile takipten çıkar. Geçmişi temizlemek isteğe bağlı; anahtarı yenilemek zorunlu.
Soru 6
Express API'nin public klasörüne hangileri girer: index.html, stil.css, uygulama.js (tarayıcıda çalışan), sunucu.js, veri.json, .env, logo.png?
Cevabı göster
index.html, stil.css, uygulama.js ve logo.png: tarayıcıya gitmesi gereken dosyalar. sunucu.js sunucunun kodu, veri.json kullanıcıların verisi, .env gizli ayarlar; üçü de public'in dışında kalır. public'teki her dosyanın bir adresi vardır ve o adresi bilen herkes onu indirir.
Soru 7
$env:PORT = "8080" yazılmış bir terminalde node -e "console.log(process.env.PORT + 1, typeof process.env.PORT)" ne yazar? (node -e, node -p gibi tırnak içindeki kodu çalıştırır, ama sonucu kendisi yazdırmaz; yazdıran console.log.)
Cevabı göster
80801 string. Ortam değişkenleri her zaman metindir; "8080" + 1 metin birleştirmesidir. Sayı gerekiyorsa Number(process.env.PORT) + 1 yazmalısın (8081).
Kayıt noktası
Projeni Git'e kaydet. kalici-api yeni bir proje olduğu için önce git init gerekiyor. Commit'ten önce derste açtığın deneme dosyalarını (dosya-deneme.js, yol-deneme.js, nerede.js, eski-kod.js, bos.js, bos.json, ortam.js, port-hesap.js, anahtar.js, goster.js, maclar klasörü, varsa kurallar.env ve ps-deneme.env) silebilirsin; cokme.js, kalabalik.js, ahri.json ve lux.json API'yi denemenin araçları, kalsınlar. git add .'ten sonra listeye bir kez bak:
Bir sonraki derste sunucun başka servislerle konuşacak: Riot'un API'si gibi anahtar isteyen bir servise .env'deki anahtarla istek atacak, cevap gecikince vazgeçmeyi, hız sınırına uymayı ve anahtarı tarayıcıdan saklayan bir ara sunucu kurmayı öğreneceksin.