# 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. - Published: 2026-06-19 - Updated: 2026-06-19 - Author: SharpSkill - Reading time: 5 min --- 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. ```typescript // src/config/typeorm.config.ts 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: ```typescript // src/app.module.ts 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('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: ```json // package.json (scripts section) { "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: ```typescript // src/entities/user.entity.ts 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; } ``` ```typescript // src/entities/order.entity.ts 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: ```typescript // src/entities/product.entity.ts 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. ## 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: ```typescript // src/orders/orders.module.ts 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: ```typescript // src/orders/orders.service.ts 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, ) {} async findByUser(userId: string): Promise { 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 { // 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: ```typescript // src/orders/orders.service.ts (additional method) async createOrderWithItems( userId: string, items: { productId: string; quantity: number }[], dataSource: DataSource, ): Promise { 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. ## 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/tr/blog/node-nestjs/nestjs-typeorm-migrations-relations-best-practices