2026'da NestJS ve TypeORM: Migration'lar, İlişkiler ve Mülakat Soruları

NestJS ve TypeORM, 2026 itibarıyla Node.js backend dünyasının en olgun kombinasyonlarından birini oluşturuyor. Bu makale migration yönetimi, ilişki modelleme, transaction kullanımı ve teknik mülakat sorularını ele alıyor.

2026'da NestJS ve TypeORM: Migration'lar, İlişkiler ve Mülakat Soruları

NestJS ve TypeORM, 2026 itibarıyla Node.js tabanlı backend geliştirmede en köklü kombinasyonlardan birini oluşturuyor. NestJS modüler mimarisi ve yerleşik bağımlılık enjeksiyonu ile öne çıkarken, TypeORM tip güvenli veritabanı operasyonları için gerekli araçları sunuyor. Ancak günlük proje pratiğinde — özellikle migration yönetimi, karmaşık ilişkiler ve iki teknolojinin birlikte çalışması konusunda — teknik mülakatlarda da düzenli olarak karşılaşılan sorular ortaya çıkıyor.

Bu makale, NestJS projelerinde TypeORM migration'larıyla ilgili temel kavramları inceliyor, çeşitli ilişki türlerini somut kod örnekleriyle açıklıyor ve konuyu derinleştiren pratik mülakat sorularıyla sonlanıyor.

TypeORM DataSource API

TypeORM, yapılandırma API'sini köklü bir şekilde yeniden tasarladı. Hâlâ ormconfig.json veya createConnection kullanan projeler DataSource API'sine geçiş yapmalıdır. Bu makaledeki tüm örnekler güncel API'yi kullanmaktadır.

NestJS'te TypeORM Yapılandırması

İlk adım, bağımsız bir DataSource yapılandırması oluşturmaktır. Bu dosya, TypeORM CLI tarafından migration işlemleri için kullanılır. Yapılandırmanın ayrılması, CLI'ın NestJS bağlamından bağımsız çalışabilmesini sağlar.

src/config/typeorm.config.tstypescript
import { DataSource } from 'typeorm';
import { config } from 'dotenv';

config(); // Load .env variables

export default new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  entities: ['src/**/*.entity.ts'],
  migrations: ['src/migrations/*.ts'],
  synchronize: false,
});

synchronize: false özelliği özel bir dikkat gerektiriyor. Geliştirme modunda TypeORM'un veritabanı şemasını otomatik güncellemesi için synchronize: true ayarlamak cazip gelebilir. Ancak üretim ortamlarında bu durum veri kaybına ve öngörülemeyen davranışlara yol açar. Bunun yerine migration'lar kullanılır.

AppModule içinde TypeORM, ConfigService üzerinden ortam değişkenlerini sağlayan asenkron yapılandırma ile entegre edilir:

src/app.module.tstypescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.get('DB_HOST'),
        port: config.getOrThrow<number>('DB_PORT'),
        username: config.get('DB_USER'),
        password: config.get('DB_PASSWORD'),
        database: config.get('DB_NAME'),
        autoLoadEntities: true, // Auto-register entities from feature modules
        synchronize: false,     // Always false in production
      }),
    }),
  ],
})
export class AppModule {}

config.get() ile config.getOrThrow() arasındaki fark ince ama önemlidir: getOrThrow(), değişken bulunamadığında bir istisna fırlatır. Veritabanı portu gibi kritik yapılandırma değerleri için bu yaklaşım, yanlış yapılandırmaların sessizce varsayılan değerler kullanılması yerine anında tespit edilmesini sağlar.

Migration'lar: Oluşturma, Çalıştırma ve Geri Alma

Migration'lar her profesyonel veritabanı stratejisinin temelini oluşturur. Şema değişikliklerini sürümlenmiş ve tekrarlanabilir biçimde belgeler. Aşağıdaki npm script'leri TypeORM CLI ile çalışmayı kolaylaştırır:

package.json (scripts section)json
{
  "migration:generate": "typeorm-ts-node-commonjs migration:generate src/migrations/$npm_config_name -d src/config/typeorm.config.ts",
  "migration:run": "typeorm-ts-node-commonjs migration:run -d src/config/typeorm.config.ts",
  "migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/config/typeorm.config.ts"
}

Pratikteki iş akışı şu şekildedir: Önce entity'ler değiştirilir, ardından npm run migration:generate --name=AddUserTable komutu mevcut şema ile entity tanımları arasındaki farkları yansıtan bir migration dosyasını otomatik olarak oluşturur. npm run migration:run migration'ı uygular, npm run migration:revert ise son migration'ı geri alır.

Projelerde sık yapılan bir hata, migration:generate karşılaştırmayı otomatik olarak yapmasına rağmen migration'ların elle yazılmasıdır. Elle yazılan migration'lar yalnızca veri dönüşümü gerektiğinde anlamlıdır — örneğin bir sütunun yeniden adlandırılıp ardından verilerin taşınması gerektiğinde.

Oluşturulan migration'ları kontrol edin

Yeniden adlandırılan bir sütun, ALTER RENAME yerine DROP + ADD işlemi üretir. Bu, mevcut verilerin kaybına neden olur. up() metodundaki SQL ifadeleri her zaman gözden geçirilmeli ve gerektiğinde elle düzeltilmelidir.

Entity ve İlişki Modelleme

TypeORM, veritabanı yapılarını doğrudan TypeScript sınıflarına eşlemek için decorator'ları kullanır. En önemli üç ilişki türü OneToMany/ManyToOne, ManyToMany ve OneToOne'dır.

OneToMany ve ManyToOne

Kullanıcılar ve siparişler arasındaki klasik ilişki, @OneToMany ve @ManyToOne decorator'larının birlikte çalışmasını gösterir:

src/entities/user.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm';
import { Order } from './order.entity';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  email: string;

  @Column()
  name: string;

  @OneToMany(() => Order, (order) => order.user)
  orders: Order[];  // No DB column created here; relation lives on the Order side

  @CreateDateColumn()
  createdAt: Date;
}
src/entities/order.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';

@Entity('orders')
export class Order {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('decimal', { precision: 10, scale: 2 })
  total: number;

  @Column({ default: 'pending' })
  status: string;

  @ManyToOne(() => User, (user) => user.orders, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'user_id' })  // Explicit FK column name
  user: User;
}

Burada kritik nokta şudur: @OneToMany tarafı veritabanında herhangi bir sütun oluşturmaz. Yabancı anahtar her zaman @ManyToOne tarafında bulunur. @JoinColumn decorator'ı yabancı anahtar sütununun adını açıkça belirler; bu da veritabanının okunabilirliğini artırır ve isim çakışmalarını önler. onDelete: 'CASCADE' seçeneği, ilişkili kullanıcı silindiğinde siparişlerin de otomatik olarak silinmesini sağlar.

ManyToMany

Ürünler ve kategoriler için ManyToMany ilişkisi uygundur. TypeORM bu durumda otomatik olarak bir ara tablo oluşturur:

src/entities/product.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Category } from './category.entity';

@Entity('products')
export class Product {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  name: string;

  @Column('decimal', { precision: 10, scale: 2 })
  price: number;

  @ManyToMany(() => Category, (category) => category.products)
  @JoinTable({ name: 'product_categories' })  // Owns the junction table
  categories: Category[];
}

@JoinTable decorator'ı ilişkinin sahip tarafını belirler. Yalnızca bir taraf bu decorator'a sahip olabilir. Ara tablonun ek sütunlar içermesi gerekiyorsa (örneğin sıralama düzeni), bunun yerine iki @ManyToOne ilişkisi içeren ayrı bir entity oluşturulmalıdır.

Node.js / NestJS mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Repository Pattern ve Feature Modülleri

NestJS, feature modülleri aracılığıyla modülerleştirmeyi teşvik eder. Her modül entity'lerini TypeOrmModule.forFeature() ile kaydeder ve ilgili repository'lere erişim kazanır:

src/orders/orders.module.tstypescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Order } from '../entities/order.entity';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';

@Module({
  imports: [TypeOrmModule.forFeature([Order])],
  providers: [OrdersService],
  controllers: [OrdersController],
})
export class OrdersModule {}

Service katmanında repository, @InjectRepository ile enjekte edilir. Basit sorgular için Repository API yeterli olurken, daha karmaşık senaryolarda QueryBuilder devreye girer:

src/orders/orders.service.tstypescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '../entities/order.entity';

@Injectable()
export class OrdersService {
  constructor(
    @InjectRepository(Order)
    private readonly orderRepo: Repository<Order>,
  ) {}

  async findByUser(userId: string): Promise<Order[]> {
    return this.orderRepo.find({
      where: { user: { id: userId } },
      relations: ['user'],          // Eager-load the user relation
      order: { createdAt: 'DESC' }, // Most recent first
    });
  }

  async findWithFilters(status: string, minTotal: number): Promise<Order[]> {
    // QueryBuilder for complex queries
    return this.orderRepo
      .createQueryBuilder('order')
      .leftJoinAndSelect('order.user', 'user')
      .where('order.status = :status', { status })
      .andWhere('order.total >= :minTotal', { minTotal })
      .orderBy('order.total', 'DESC')
      .getMany();
  }
}

findByUser metodu, iç içe Where koşullarıyla ilişkilerin nasıl filtreleneceğini gösterir. relations dizisi hangi bağlantıların yükleneceğini kontrol eder. Bu bildirim olmadan TypeORM yalnızca siparişleri döndürür, kullanıcı verilerini dahil etmez.

findWithFilters içindeki QueryBuilder daha fazla kontrol sunar: leftJoinAndSelect ilişkiyi yükler ve sonuçlarda erişilebilir kılar; parametrize edilmiş sorgular ise SQL injection saldırılarını güvenilir biçimde önler.

Transaction'ların Doğru Kullanımı

Birden fazla veritabanı işleminin atomik olarak gerçekleştirilmesi gerektiğinde transaction'lar vazgeçilmezdir. TypeORM bu amaçla DataSource.transaction() metodunu sunar:

src/orders/orders.service.ts (additional method)typescript
async createOrderWithItems(
  userId: string,
  items: { productId: string; quantity: number }[],
  dataSource: DataSource,
): Promise<Order> {
  return dataSource.transaction(async (manager) => {
    // All operations use the transactional manager
    const order = manager.create(Order, {
      user: { id: userId },
      total: 0,
      status: 'pending',
    });
    const savedOrder = await manager.save(order);

    let total = 0;
    for (const item of items) {
      const product = await manager.findOneByOrFail(Product, { id: item.productId });
      total += product.price * item.quantity;

      await manager.save(OrderItem, {
        order: savedOrder,
        product,
        quantity: item.quantity,
        unitPrice: product.price,
      });
    }

    savedOrder.total = total;
    return manager.save(savedOrder);
  });
}

Transaction içinde tüm işlemler için yalnızca manager kullanılır. Bunun yerine standart repository kullanılsaydı, bu işlemler transaction dışında gerçekleşir ve sonraki bir adımdaki hata tutarsız verilere yol açabilirdi. Callback içinde bir hata oluştuğunda transaction'ın tamamı otomatik olarak geri alınır.

QueryRunner vs transaction()

DataSource.transaction() basit çok adımlı işlemler için uygundur. QueryRunner ise commit/rollback noktaları ve savepoint'ler üzerinde elle kontrol imkânı tanır — toplu işlemlerde kısmi geri almalar için faydalıdır.

NestJS ve TypeORM Hakkında Sık Sorulan Mülakat Soruları

NestJS ve TypeORM konularındaki teknik mülakatlar genellikle üç alanı kapsar: mimari anlayış, pratik problem çözme ve yaygın hatalardan kaçınma.

synchronize neden üretim ortamında devre dışı bırakılmalıdır?

synchronize: true seçeneği, her uygulama başlangıcında veritabanı şemasını entity tanımlarıyla otomatik olarak senkronize eder. Bu durum sütunları silebilir, veri tiplerini değiştirebilir veya tabloları kaldırabilir — üstelik hiçbir belgeleme olmadan. Üretim ortamlarında bu veri kaybına yol açar. Migration'lar ise sürümlenmiş, tekrarlanabilir ve geri alınabilir şema değişiklikleri sunar.

migration:generate ile migration:create arasındaki fark nedir?

migration:generate komutu mevcut entity tanımlarını veritabanı şemasıyla karşılaştırır ve gerekli SQL ifadelerini otomatik olarak üretir. migration:create ise SQL komutlarının elle yazıldığı boş bir migration dosyası oluşturur. İkincisi, veri migration'ları veya seed işlemleri için kullanışlıdır.

OneToMany/ManyToOne ilişkisinde yabancı anahtar hangi taraftadır?

Yabancı anahtar her zaman @ManyToOne tarafındadır. @OneToMany tarafı veritabanında herhangi bir sütun oluşturmaz; yalnızca TypeScript'te ters ilişkiyi temsil etmeye yarar.

QueryBuilder ne zaman Repository API yerine kullanılmalıdır?

Repository API (find, findOne, save) basit CRUD işlemleri ve doğrudan filtre koşullarına sahip sorgular için uygundur. QueryBuilder ise karmaşık join'ler, alt sorgular, toplama fonksiyonları veya dinamik olarak oluşturulan sorgular gerektiğinde kullanılır.

Modüller arasındaki dairesel bağımlılıklar nasıl çözülür?

NestJS, modüller arasındaki dairesel bağımlılıklar için forwardRef() sağlar. Ancak bunun kullanılması genellikle bir mimari soruna işaret eder. Daha iyi yaklaşım, ortak kullanılan mantığı ayrı bir modüle taşımaktır.

Bir transaction içinde manager yerine standart repository kullanılırsa ne olur?

Standart repository üzerinden gerçekleştirilen işlemler transaction dışında çalışır. Bu, değişikliklerin anında commit edildiği ve hata durumunda geri alınamayacağı anlamına gelir. Sonuç olarak veri tutarsızlıklarına yol açar.

@JoinColumn ile @JoinTable arasındaki fark nedir?

@JoinColumn, OneToOne ve ManyToOne ilişkilerinde yabancı anahtar sütununun sahibi olan tarafı belirtmek için kullanılır. @JoinTable ise yalnızca ManyToMany ilişkilerinde kullanılır ve her iki tarafı birbirine bağlayan ara tabloyu (junction table) tanımlar.

TypeORM'da ilişki yükleme stratejileri nelerdir?

TypeORM iki temel strateji sunar: eager loading (ilişki her sorguda otomatik olarak yüklenir) ve lazy loading (ilişki ilk erişimde yüklenir, Promise kullanımı gerektirir). Pratikte en yaygın yaklaşım, yüklenecek ilişkilerin relations dizisi veya QueryBuilder aracılığıyla açıkça belirtilmesidir.

TypeOrmModule'da forRoot ile forFeature arasındaki fark nedir?

forRoot() veritabanı bağlantısını yapılandırır ve ana modülde yalnızca bir kez çağrılır. forFeature() ise entity'leri ilgili feature modülü bağlamında kaydeder ve ilgili repository'lerin enjekte edilebilmesini sağlar.

TypeORM'da save ile insert arasındaki fark nedir?

save, entity'nin zaten var olup olmadığını kontrol eder (birincil anahtara göre) — varsa günceller, yoksa yeni kayıt oluşturur. insert ise her zaman yeni bir kayıt oluşturur ve ön sorgu gerektirmediğinden daha hızlıdır. Ancak insert, entity subscriber'larını veya kademeli kayıtları tetiklemez.

Node.js / NestJS mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Sonuç

NestJS ve TypeORM kombinasyonu, Node.js ile tip güvenli backend geliştirme için olgun bir ekosistem sunuyor. Temel çıkarımlar şu şekilde özetlenebilir:

  • Migration'lar, yerel geliştirme dışındaki her ortamda synchronize'ın yerini alır
  • İlişkiler, yabancı anahtarın hangi tarafta olduğunun net bir şekilde anlaşılmasını gerektirir
  • Transaction'lar tutarlı biçimde manager üzerinden yürütülmeli, standart repository'ler kullanılmamalıdır
  • QueryBuilder, basit metodların yetersiz kaldığı yerlerde Repository API'yi tamamlar
  • Teknik mülakatlarda yalnızca söz dizimi bilgisi değil, mimari kararların ardındaki gerekçeler de değerlendirilir

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

Paylaş

İlgili makaleler