Mert Özen Her Satırda Daha İleri
Backend Node.js Temelleri — #02

Node.js Kurulumu ve İlk Proje: Version Manager, .nvmrc, package.json, node --watch ve --env-file

Mert Özen 14 Kas 2026 10 dk 8 görüntülenme
Node.js Kurulumu ve İlk Proje: Version Manager, .nvmrc, package.json, node --watch ve --env-file

Node.js projesine başlamanın doğru yolu Node'u bir version manager ile kurmak, sürümü .nvmrc ve package.json'daki engines alanıyla sabitlemek ve script'leri package.json'a yazmaktır. Gerçek çıktılarla engine-strict'in Node 24'te verdiği EBADENGINE hatası, npm init ve package.json anatomisi, node --watch ile yeniden başlatma, --env-file'da shell değerinin .env'i ezmesi, node --run ile npm run arasındaki 0,04 / 0,11 saniyelik fark ve Bookstore API'sinin ilk GET /health endpoint'i.

Sürüm notu: Bu yazıdaki örnekler Node.js 26.13.1 (LTS) ve npm 11.20.0 ile çalıştırılmıştır; engines denemesinde ayrıca Node.js 24.22.3 kullanılmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) Node.js API'si bu yazıda ilk kez çalışacak. Sunucu, dinleyeceği portu environment variable'dan okuyor:

const port = Number(process.env.PORT ?? 3000);

Proje klasöründe bir .env dosyası var:

PORT=3000
NODE_ENV=development

Terminalde PORT'u ayrıca 4000 yapıp sunucuyu --env-file ile başlatıyorsun:

PORT=4000 node --env-file=.env src/server.js

Sunucu hangi portu dinler? .env dosyası, adı üstünde, ayarları yüklemek için var; sona yüklenen kazanır diye düşünürsen 3000, shell'deki değer kazanır diye düşünürsen 4000.

Bookstore API listening on http://localhost:4000 (development)

Shell kazandı. Node dokümanı bunu açıkça yazıyor: aynı değişken hem ortamda hem dosyada tanımlıysa ortamdaki değer geçerli olur. Bu davranış production için doğru tercih, ama geliştirirken ".env'i değiştirdim, neden etkisi yok?" sorusunun da en sık cevabı. Bu yazıda Node'u version manager ile kuruyoruz, Bookstore API projesini package.json ile başlatıyoruz ve ASP.NET Core'dan alışık olduğun dotnet watch, global.json ve appsettings alışkanlıklarının Node'daki karşılıklarına bakıyoruz.

Node'u Version Manager ile Kurmak

nodejs.org'dan indirilen installer bilgisayara tek bir Node sürümü kurar. Tek proje için yeterli, ama iki projen farklı major sürümler istediğinde ya da yeni bir LTS'e geçmeden önce denemek istediğinde zorlaşır. .NET'te SDK'lar yan yana kurulur ve hangisinin kullanılacağına global.json karar verir. Node'da bu işi version manager yapar: birden fazla Node sürümünü kullanıcı klasöründe tutar ve klasöre göre doğru olanı PATH'e (işletim sisteminin komutları aradığı klasör listesi) koyar.

Windows'ta en pratik seçenek fnm (Fast Node Manager). Rust ile yazılmış, tek binary, PowerShell'i destekliyor ve hem .nvmrc hem .node-version dosyalarını okuyor. Kurulum ve ilk kullanım şöyle; bu komutları Windows'ta çalıştırmadım, fnm'in README'sinden aldım:

# Windows: fnm'i winget ile kur
winget install Schniz.fnm
# PowerShell profiline ekle: klasör değişince .nvmrc'yi okusun
fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression
# .nvmrc'deki sürümü kur ve kullan
fnm install
fnm use

macOS ve Linux'ta fnm ya da nvm kullanılır; nvm bir shell script'idir ve Windows'ta çalışmaz. Windows için ayrı bir proje olan nvm-windows var, ama komutları ve davranışı nvm ile aynı değil. Hangisini seçersen seç, kural aynı: Node'u ve global araçları yönetici yetkisiyle sistem klasörüne kurma, sürümü projenin içindeki bir dosya belirlesin.

Node 25'ten beri değişen bir şey daha var: corepack artık Node ile gelmiyor. Node 25.0.0 sürüm notlarında corepack'in dağıtımdan çıkarıldığı yazıyor; 26.13.1'in bin klasöründe de yalnız node, npm ve npx var. pnpm ya da Yarn kullanacaksan corepack'i npm'den ayrıca kurman ya da o aracı kendi yöntemiyle kurman gerekiyor. Bu seride npm kullanıyoruz.

.nvmrc ve engines: global.json'un Karşılığı

Sürüm sabitlemenin iki katmanı var. Birincisi .nvmrc: version manager'lara hangi Node'u kullanacaklarını söyleyen tek satırlık bir dosya. GitHub Actions'taki setup-node da bu dosyayı okuyabiliyor, böylece geliştirici makinesi ile CI aynı sürümü kullanır.

26

Yalnız major sürüm yazdım: version manager 26'nın en güncel kurulu sürümünü seçer, güvenlik sürümleri için dosyayı değiştirmek gerekmez. Tam sürüm (26.13.1) yazmak daha kesin ama her yamada güncelleme ister; ekip tercihine göre ikisi de olur.

İkinci katman package.json'daki engines alanı. Bu alan npm'e projenin hangi Node aralığında çalıştığını söyler. Varsayılan olarak npm uyumsuzlukta yalnız uyarı verir; projenin köküne bir .npmrc koyup engine-strict=true yazarsan hata verir:

engine-strict=true

Aynı projede Node 24 ile npm install denedim:

npm error code EBADENGINE
npm error engine Unsupported engine
npm error engine Not compatible with your version of node/npm: bookstore-api@0.1.0
npm error notsup Not compatible with your version of node/npm: bookstore-api@0.1.0
npm error notsup Required: {"node":"^26.13.0"}
npm error notsup Actual:   {"node":"v24.22.3","npm":"11.19.1"}

Dikkat: bu kontrolü npm yapar, Node değil. Aynı makinede node src/server.js Node 24 ile çalışmaya devam eder. global.json'da SDK uymazsa dotnet komutu hiç çalışmaz; Node'da ise koruma yalnız paket kurulumunda devreye giriyor. Asıl güvenceyi CI'da .nvmrc'deki sürümü kullanmak verir.

npm init ve package.json Anatomisi

package.json, .NET'teki .csproj'un karşılığı gibi düşünülebilir: projenin adı, sürümü, bağımlılıkları ve çalıştırılabilir script'leri burada. Boş bir klasörde npm init -y varsayılanlarla bir dosya üretir:

Wrote to /home/user/bookstore-api/package.json:

{
  "name": "bookstore-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs"
}

npm 11 artık "type": "commonjs"'i açıkça yazıyor. Bu satır projedeki .js dosyalarının hangi module sistemiyle yükleneceğini belirliyor ve bir sonraki yazının konusu. Bookstore API'sinde dosyayı şöyle düzenledim:

{
  "name": "bookstore-api",
  "version": "0.1.0",
  "private": true,
  "description": "Bookstore API on Node.js",
  "type": "module",
  "main": "src/server.js",
  "engines": {
    "node": "^26.13.0"
  },
  "scripts": {
    "start": "node src/server.js",
    "dev": "node --watch --env-file-if-exists=.env src/server.js",
    "versions": "node -p process.versions.node"
  }
}

private: true paketin yanlışlıkla npm registry'ye publish edilmesini engeller; uygulama projelerinde hep olmalı. "type": "module" ile .js dosyaları ECMAScript module olarak, yani JavaScript standardının kendi module sistemiyle yüklenir, yani import/export kullanırız. main, paketin giriş noktası; uygulama için daha çok belgeleme amaçlı. scripts ise npm run ile çalıştırılan komutlar: dotnet run ya da launchSettings.json'daki profillerin işini görür. dependencies ve devDependencies henüz yok; paket ekledikçe npm onları yazacak, ayrıntısı 04'te.

İlk npm install bağımlılık olmasa da bir package-lock.json üretir. Onu da commit'liyoruz; neden önemli olduğunu 04'te göreceğiz.

REPL: Hızlı Deneme İçin

Argümansız node komutu bir REPL (Read-Eval-Print Loop; yazdığın satırı hemen çalıştırıp sonucu gösteren etkileşimli konsol) açar. C# Interactive ya da dotnet-script ile aynı amaca hizmet eder: bir API'nin ne döndürdüğünü dosya açmadan görmek.

Welcome to Node.js v26.13.1.
Type ".help" for more information.
> const price = 349.9
undefined
> new Intl.NumberFormat("tr-TR", { style: "currency", currency: "TRY" }).format(price)
'₺349,90'
> [1, 2, 3].map(n => n * price)
[ 349.9, 699.8, 1049.6999999999998 ]
> _
[ 349.9, 699.8, 1049.6999999999998 ]

_ son ifadenin sonucunu tutar. Son satırdaki 1049.6999999999998 da para hesabında neden number ile dikkatli olman gerektiğini hatırlatıyor; Bookstore'da fiyatları kuruş cinsinden tamsayı olarak tutmayı ilerideki yazılarda konuşacağız. REPL'den .exit ya da iki kez Ctrl+C ile çıkılır.

İlk Endpoint: GET /health

Proje iskeleti, ASP.NET Core Web API serisindeki katmanlara paralel olacak: routes HTTP'yi bilir, services iş kuralını, db veri erişimini. Bu yazıda yalnız routes dolu, diğer klasörler boş bekliyor:

bookstore-api/
  .env.example
  .gitignore
  .npmrc
  .nvmrc
  package.json
  package-lock.json
  src/
    server.js
    routes/health.js
    services/
    db/
// GET /health: process ayakta mı, hangi Node sürümüyle çalışıyor?
export function handleHealth(req, res) {
    const body = {
        status: "ok",
        service: "bookstore-api",
        node: process.version,
        uptimeSeconds: Math.round(process.uptime()),
    };
    res.writeHead(200, { "content-type": "application/json" });
    res.end(JSON.stringify(body));
}
import { createServer } from "node:http";
import { handleHealth } from "./routes/health.js";

const port = Number(process.env.PORT ?? 3000);

const server = createServer((req, res) => {
    if (req.method === "GET" && req.url === "/health") {
        return handleHealth(req, res);
    }
    res.writeHead(404, { "content-type": "application/json" });
    res.end(JSON.stringify({ error: "not found" }));
});

server.listen(port, () => {
    console.log(`Bookstore API listening on http://localhost:${port} (${process.env.NODE_ENV ?? "development"})`);
});
npm run dev
curl http://localhost:3000/health
{"status":"ok","service":"bookstore-api","node":"v26.13.1","uptimeSeconds":1}

Framework yok, routing iki if'ten ibaret. Bu bilinçli: node:http'nin request ve response object'lerini 10'da ayrıntılı göreceğiz, Express'e de 11'de geçeceğiz. Şimdilik amaç projenin kendisi.

node --watch: dotnet watch Karşılığı

dev script'i Node'u --watch ile başlatıyor. Bu modda Node giriş dosyasını ve onun import ettiği bütün module'leri izler; biri değişince process'i yeniden başlatır. Sunucu açıkken health.js'e bir alan ekledim:

Bookstore API listening on http://localhost:3000 (development)
Change detected in '/home/user/bookstore-api/src/routes/health.js'
Restarting 'src/server.js'
Bookstore API listening on http://localhost:3000 (development)
{"status":"ok","checkedAt":"2026-11-14T17:53:12.917Z","service":"bookstore-api","node":"v26.13.1","uptimeSeconds":1}

uptimeSeconds'ın yine 1 olması, değişikliğin yeni bir process ile geldiğini gösteriyor. dotnet watch'taki hot reload'dan farklı olarak burada state korunmaz; bellekteki her şey sıfırlanır. Bir API için bu genelde sorun değil. --watch Node 22'den beri stable; nodemon gibi ayrı bir pakete artık gerek yok. Import edilmeyen dosyaları da izlemek istersen --watch-path ile klasör verebilirsin. Production'da --watch kullanılmaz.

--env-file: .env Dosyası ve appsettings Farkı

--env-file, Node 24.10'dan beri deneysel değil. Dosyadaki her KEY=value satırını process.env'e yükler; # ile başlayan satırlar yorumdur, tırnaklı ve çok satırlı değerler desteklenir. Birden fazla --env-file verebilirsin, sonraki dosya öncekinin değerini ezer. Ama Önce Tahmin Et'te gördüğümüz gibi, ortamda zaten tanımlı bir değişkeni ezmez. dev script'inde kullandığım --env-file-if-exists ise dosya yoksa hata vermeden devam eder; --env-file dosya yoksa process'i başlatmaz. Kod içinden yüklemek gerekirse process.loadEnvFile() da var.

ASP.NET Core'un configuration sistemiyle farkı büyük. appsettings.json hiyerarşik bir yapı, tipli Options binding'i ve environment'a göre katmanlama sunar; User Secrets geliştirici makinesindeki gizli değerleri repo dışında tutar. .env ise düz bir string listesi; tip yok, doğrulama yok, katman yok. Bu yüzden iki kural koyuyoruz. Birincisi, .env commit'lenmez, onun yerine değerleri boş bir template commit'lenir:

# Kopyalayıp .env olarak kaydet; .env commit'lenmez
PORT=3000
NODE_ENV=development
node_modules/
.env
*.log

İkincisi, .env yalnız geliştirme içindir. Production'da değerler platformdan, yani container ortamından, Kubernetes secret'larından ya da bir secret store'dan gelir. Configuration'ı şemayla doğrulanan tipli bir object'e çevirmeyi 14'te, Options pattern karşılığı olarak kuracağız.

node --run ve npm run

Script'leri çalıştırmanın iki yolu var. npm run uzun süredir standart; Node 22'den beri node --run de var:

> bookstore-api@0.1.0 versions
> node -p process.versions.node

26.13.1
26.13.1

İlk iki satır ve ilk 26.13.1 npm run versions'ın çıktısı, son satır node --run versions'ın. Aynı script'i üç kez zamanladım: npm run her seferinde 0,11–0,12 saniye, node --run 0,04 saniye sürdü. Fark, npm'in kendisinin bir Node programı olarak yüklenmesinden geliyor. node --run'ı argümansız çalıştırınca script listesini yazar:

Available scripts are:
  start: node src/server.js
  dev: node --watch --env-file-if-exists=.env src/server.js
  versions: node -p process.versions.node

Hız bedava değil. Node dokümanına göre node --run bilerek sınırlı tutuluyor: prestart/poststart gibi pre ve post script'lerini çalıştırmaz, npm'e özgü environment variable'ları tanımlamaz. Script'lerin bu özelliklere dayanıyorsa npm run'da kal. Bu projede ikisini de kullanabiliriz; yazılarda okunabilirlik için npm run yazacağım.

Üretimde Dikkat

Production'da process'i doğrudan node src/server.js ile başlat; ne --watch ne --env-file. Container içinde npm start ile başlatmak, npm'i process ile sinyaller arasına sokar; SIGTERM'in (process'e kapanmasını söyleyen sinyal) Node'a ulaşması ve graceful shutdown konusu 14'te ve Docker serisinde.

NODE_ENV=production'ı platformda tanımla. Node'un kendisi bu değişkene özel bir anlam yüklemez, ama Express gibi pek çok library davranışını buna göre değiştirir. Bunun etkisini Express'e geçince ölçeceğiz.

CI'da Node sürümünü .nvmrc'den oku ve engine-strict'i açık tut. Geliştiricinin makinesinde 24, CI'da 26 çalışan bir proje, ilk hatayı en kötü yerde verir.

Global kurulum yapma. Projenin ihtiyaç duyduğu her araç (TypeScript, test runner, linter) devDependencies'te durur ve npx ya da script'lerle çalışır. Böylece projeyi klonlayan herkes aynı sürümü kullanır.

Sık Düşülen Tuzaklar

Global npm paketlerine bağımlı kalmak. npm install -g typescript ile kurulmuş bir tsc senin makinende çalışır, CI'da yoktur ya da başka sürümdür. Script'ler yalnız projenin kendi node_modules/.bin'indeki araçları kullanmalı; npm run ve node --run o klasörü PATH'e zaten ekliyor.

.env'i commit'lemek. Bir kez commit'lenen değer git geçmişinde kalır; dosyayı silmek yetmez, değeri değiştirmek (rotate etmek) gerekir. .gitignore'u projenin ilk commit'inde ekle.

.env'in her şeyi ezdiğini sanmak. Shell'de ya da CI'da tanımlı bir değişken dosyadakini ezer; beklenmedik bir değer görüyorsan önce ortamı kontrol et.

node --run'a geçip pre/post script'lerinin sessizce atlandığını fark etmemek. prebuild gibi bir adımın varsa ya onu ana script'e taşı ya da npm run'da kal.

engines'i koruma sanmak. Sadece npm'i durdurur; yanlış Node ile node komutu çalışmaya devam eder.

Küçük Bir Deneme

★☆☆ .env.development ve .env.local adında iki dosya oluştur, ikisine farklı PORT değerleri yaz ve node --env-file=.env.development --env-file=.env.local src/server.js ile başlat. Hangi port kazanıyor? Sırayı değiştirince ne oluyor?

★★☆ package.json'a "prestart": "node -e \"console.log('checking config')\"" ekle. npm start ile node --run start çıktılarını karşılaştır.

★★★ /health cevabına package.json'daki version değerini ekle. Dosyayı her istekte okumadan, process başlarken bir kez yükle. İpucu: bir sonraki yazıda göreceğimiz JSON import attribute'larını ya da node:fs'i kullanabilirsin.

Proje ayakta, ama import satırlarında neden .js uzantısı yazdığımızı ve "type": "module"'ün tam olarak neyi değiştirdiğini konuşmadık. Bir sonraki yazıda Node'un iki module sistemini, CommonJS ve ECMAScript module'ü, exports ve imports alanlarını ve artık varsayılan olan require(esm)'i ele alıyoruz.