NestJS Mimarisi: Node.js Dünyasının Spring'i
NestJS, Node.js üzerinde çalışan, TypeScript'i birinci sınıf vatandaş olarak kabul eden bir uygulama çatısıdır. Angular'dan ilham alan modül/decorator yapısı ve bağımlılık enjeksiyonu (DI) yaklaşımı, onu özellikle Java/Spring ekosisteminden gelen geliştiriciler için tanıdık kılar. Bu rehber, orta seviye bir Node.js geliştiricisinin NestJS'in mimari temellerini doğru şekilde kurmasına odaklanır.
Önkoşullar ve kapsam
- Node.js ve npm (veya pnpm/yarn) kurulu olmalı.
- TypeScript, decorator ve modül kavramlarına aşinalık beklenir.
- DI (Dependency Injection) kavramını en azından duymuş olmanız gerekir.
- Bu metin belirli bir NestJS sürüm numarasına bağlı kalma iddiası taşımaz. Decorator ve modül API'leri uzun süredir kararlıdır; sürüme özel kırıcı değişiklikler için resmi dokümantasyonu ve sürüm notlarını kontrol edin.
Neden "Node.js'in Spring'i" deniyor?
Spring'in üç temel taşı vardır: kapsayıcı (container), bağımlılık enjeksiyonu ve katmanlı mimari. NestJS bunları şu şekilde karşılar:
- Modüller (
@Module): Uygulamayı dikey dilimlere böler. Her modül kendi controller, provider ve dışa aktarımlarını tanımlar. - Provider'lar ve DI: Sınıflar
@Injectable()ile işaretlenir; Nest bunları bir IoC kapsayıcısında yönetir ve constructor üzerinden enjekte eder. - Katmanlar: Controller (HTTP/giriş), Service (iş mantığı), Repository/Veri erişimi ayrımı doğal olarak teşvik edilir.
Bu yapı, kodun test edilebilirliğini ve modüller arası bağımlılıkların yönetilebilirliğini artırır.
Adım 1: Modül, Controller ve Provider üçlüsü
Bir kaynağı (örneğin kullanıcıları) yöneten tipik bir modül şu parçalardan oluşur:
// users.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
private readonly users = new Map<number, { id: number; name: string }>();
findOne(id: number) {
return this.users.get(id);
}
create(name: string) {
const id = this.users.size + 1;
const user = { id, name };
this.users.set(id, user);
return user;
}
}
// users.controller.ts
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(Number(id));
}
@Post()
create(@Body('name') name: string) {
return this.usersService.create(name);
}
}
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Beklenen davranış: UsersService constructor'da enjekte edilir; UsersController örneği Nest tarafından oluşturulurken bağımlılık otomatik çözülür. @Get(':id') isteği geldiğinde id string olarak gelir; Number() dönüşümü burada açıkça yapılır.
Bu örnek, kalıcı bir veritabanı kullanmaz; amaç DI ve katman ayrımını göstermektir.
Adım 2: Modül sınırları ve dışa aktarım
Bir modülün provider'ı yalnızca kendi içinde kullanılabilir. Başka bir modülde kullanmak için iki adım gerekir:
- Kaynak modül provider'ı
exportsdizisine ekler. - Tüketen modül, kaynak modülü
importsdizisine ekler.
@Module({
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
@Module({
imports: [UsersModule],
providers: [AuthService],
})
export class AuthModule {}
AuthService artık UsersService'i constructor üzerinden enjekte edebilir. Yaygın hata: exports eklemeden başka modülden enjekte etmeye çalışmak; Nest çalışma zamanında bağımlılığı çözemez ve uygulama başlangıçta hata verir.
Adım 3: Global modüller ve yapılandırma
Veritabanı bağlantısı, yapılandırma veya loglama gibi her yerde ihtiyaç duyulan servisler için @Global() kullanılabilir. Ancak global modüller, modül sınırlarını görünmez kılar ve bağımlılık grafiğini okumayı zorlaştırır. Zorunlu olmadıkça tercih etmeyin.
Yapılandırma için @nestjs/config paketi, ortam değişkenlerini şema doğrulamasıyla okumaya izin verir. Bu paketin API'si sürüme göre değişebileceğinden kurulum sonrası resmi dokümantasyondaki kullanım biçimini teyit edin.
Adım 4: Middleware, Guard, Interceptor, Pipe, Filter
Nest'in istek yaşam döngüsü katmanları sırayla şöyle işler:
- Middleware: Ham istek/yanıt üzerinde çalışır (Express/Fastify middleware'i gibi).
- Guard: İsteğin işlenip işlenmeyeceğine karar verir (yetkilendirme).
- Interceptor: Metot öncesi/sonrası mantık (loglama, dönüşüm, süre ölçümü).
- Pipe: Girdi doğrulama ve dönüştürme.
- Controller/Provider: İş mantığı.
- Exception Filter: Fırlatılan hataların yanıta dönüştürülmesi.
Bu sıralamayı bilmek, "yetkilendirme neden validation'dan önce çalışıyor?" gibi soruları yanıtlar. Guard'lar controller'dan önce çalıştığı için, yetkisiz isteklerde gereksiz doğrulama maliyetinden kaçınılır.
Adım 5: Doğrulama ve DTO'lar
Girdi doğrulaması için class-validator ve class-transformer ile birlikte ValidationPipe kullanılır:
export class CreateUserDto {
@IsString()
@Length(2, 50)
name: string;
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto.name);
}
ValidationPipe global olarak app.useGlobalPipes(new ValidationPipe({ whitelist: true })) şeklinde kaydedildiğinde, DTO'da tanımlı olmayan alanlar otomatik olarak ayıklanır. whitelist seçeneği olmadan fazladan alanlar servise kadar ulaşır; bu, mass-assignment benzeri sorunlara kapı aralayabilir.
Güvenlik notu: Doğrulama, iş mantığı katmanında yapılan yetki kontrollerinin yerini almaz. DTO yalnızca girdinin şeklini ve tipini doğrular; "bu kullanıcı bu kaynağa erişebilir mi?" sorusu Guard veya servis içi kontrole aittir.
Adım 6: Test edilebilirlik
DI'ın en somut faydası testlerde görülür. Test.createTestingModule ile bir modülün bağımlılıkları izole edilebilir:
const moduleRef = await Test.createTestingModule({
providers: [
UsersService,
{ provide: 'REPOSITORY', useValue: fakeRepo },
],
}).compile();
const service = moduleRef.get(UsersService);
Bu örnekte gerçek veritabanı yerine sahte bir repository enjekte edilir. Bu sayede servis, altyapıdan bağımsız test edilir.
Yaygın hatalar
- Provider'ı
exportsetmeden başka modülde kullanmaya çalışmak. Uygulama başlangıçta bağımlılık çözümleme hatası verir. - Her şeyi global yapmak. Modül sınırları bulanıklaşır, testlerde izolasyon zorlaşır.
- İş mantığını controller'a yazmak. Controller ince kalmalı; mantık servise taşınmalıdır.
ValidationPipe'ıwhitelistolmadan kullanmak. Fazladan alanlar sessizce geçer.- Senkron ağır işlemleri istek döngüsünde yapmak. Node.js tek iş parçacıklı event loop'a sahiptir; CPU-yoğun işler için worker thread veya kuyruk düşünülmelidir.
Güvenlik etkileri
- Guard'ları yetkilendirme için tek savunma hattı sanmak. Servis katmanında da kaynak sahipliği kontrolü yapılmalıdır (IDOR'a karşı).
- Hata mesajlarında iç detayları sızdırmak. Exception Filter ile dışarıya dönen mesajlar sadeleştirilmelidir.
- CORS ve CSRF ayarlarını varsayılan bırakmak. Tarayıcı tabanlı istemciler için bu ayarlar açıkça yapılandırılmalıdır.
- Ortam değişkenlerini doğrulamadan okumak. Eksik bir değişken uygulamayı çalışma zamanında beklenmedik şekilde bozabilir; şema doğrulaması kullanın.
Sonuç
NestJS'in gücü, Angular'dan devraldığı modüler ve DI tabanlı yapısından gelir. Modül sınırlarını doğru çizmek, katmanları amaçlarına uygun kullanmak ve doğrulamayı yetkilendirmeyle karıştırmamak, uzun ömürlü bir kod tabanının temelini oluşturur. Spring deneyiminiz varsa geçiş hızlı olacaktır; yoksa da kavramlar küçük ve tutarlı olduğu için öğrenme eğrisi yönetilebilir düzeydedir.