NestJS dan TypeORM di 2026: Migrasi, Relasi, dan Pertanyaan Wawancara

Kuasai integrasi NestJS TypeORM dengan migrasi TypeORM 1.1, relasi entitas, repository pattern, dan pertanyaan wawancara umum untuk backend developer.

NestJS dan TypeORM: Migrasi, Relasi, dan Pertanyaan Wawancara

NestJS TypeORM tetap menjadi salah satu integrasi ORM paling teruji untuk backend Node.js enterprise. Dengan TypeORM mencapai versi 1.0 pada Mei 2026, diikuti oleh seri patch 1.1.x yang memperbaiki penanganan koneksi dan persistensi relasi, serta NestJS 12 yang menyelesaikan migrasi ESM-nya, stack ini telah matang menjadi fondasi yang stabil untuk aplikasi produksi. Panduan ini membahas pola-pola esensial yang harus dikuasai setiap backend developer: migrasi, relasi entitas, repository pattern, dan pertanyaan wawancara yang menyertainya.

NestJS 12 dan TypeORM 1.1

NestJS 12 menyediakan paket core sebagai ESM, memerlukan Node.js 20.19+ atau 22.12+. Paket @nestjs/typeorm v12 mengikuti hal yang sama. TypeORM 1.1.2 memperbaiki kebocoran koneksi pada DataSource.initialize() dan mengoreksi persistensi junction row many-to-many ketika relasi tidak dimuat.

Menyiapkan Migrasi TypeORM di NestJS 12

Aturan pertama untuk database produksi: jangan pernah menggunakan synchronize: true. Auto-sync membandingkan metadata entitas dengan skema live dan menerapkan perubahan secara langsung, yang berisiko kehilangan data pada penghapusan kolom, perubahan tipe, atau field yang diubah namanya. Migrasi menyediakan alternatif yang terversi, dapat ditinjau, dan dapat dibalik.

TypeORM 1.x memerlukan file konfigurasi DataSource khusus untuk CLI, terpisah dari konfigurasi modul NestJS. CLI tidak dapat menyelesaikan dependency injection NestJS, sehingga memerlukan entry point mandiri.

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,
});

Konfigurasi modul NestJS mencerminkan pengaturan ini tetapi menggunakan pola TypeOrmModule.forRootAsync dengan dependency injection:

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 {}

autoLoadEntities: true menggantikan array entities manual dengan secara otomatis mendaftarkan setiap entitas yang diimpor melalui TypeOrmModule.forFeature() di modul fitur.

Alur Kerja Migrasi: Generate, Review, Run

TypeORM 1.x menyediakan typeorm-ts-node-commonjs sebagai CLI runner yang direkomendasikan, menggantikan pendekatan lama ts-node ./node_modules/typeorm/cli. Tambahkan script berikut ke package.json:

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"
}

Perintah generate membandingkan metadata entitas dengan database live dan menghasilkan statement SQL. Selalu tinjau output sebelum commit. TypeORM mendeteksi penambahan kolom, penghapusan, perubahan tipe, modifikasi indeks, dan perubahan foreign key, tetapi tidak menangkap backfill data, rename tabel (yang muncul sebagai drop diikuti create), dan hal yang melibatkan ekstensi database atau trigger.

Tinjau setiap migrasi yang dihasilkan

Kolom yang diubah namanya menghasilkan DROP + ADD, bukan ALTER RENAME. Ini menghancurkan data yang ada. Selalu periksa SQL di method up() dan sesuaikan secara manual jika diperlukan.

Relasi Entitas: OneToMany, ManyToOne, ManyToMany

Relasi entitas TypeORM memetakan langsung ke foreign key SQL menggunakan decorator. Memahami tiga tipe relasi inti dan opsinya merupakan hal fundamental untuk setiap modul database NestJS.

User dengan beberapa record Order mendemonstrasikan pola paling umum:

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;
}

Sisi @ManyToOne memiliki foreign key. @JoinColumn bersifat opsional pada @ManyToOne (TypeORM menyimpulkannya), tetapi penamaan eksplisit menghindari kejutan ketika nama kolom penting untuk raw query atau diff migrasi.

Untuk relasi many-to-many, TypeORM membuat junction table secara otomatis:

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 harus muncul tepat di satu sisi relasi. Ini mendefinisikan entitas mana yang memiliki junction table dan mengontrol namanya.

Siap menguasai wawancara Node.js / NestJS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Repository Pattern dan Custom Repository

Sistem modul dan DI NestJS mengintegrasikan repository TypeORM melalui TypeOrmModule.forFeature(). Menginjeksi Repository<T> default mencakup operasi CRUD standar. Ketika query menjadi kompleks, custom repository mengenkapsulasi logika tersebut.

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 {}
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();
  }
}

API find() menangani query sederhana. QueryBuilder menangani join, subquery, agregasi, dan apa pun yang memerlukan kontrol SQL yang lebih detail. Menggabungkan keduanya dalam service yang sama sepenuhnya valid: gunakan find() untuk lookup sederhana dan QueryBuilder ketika query tidak dapat diekspresikan melalui options API.

Manajemen Transaksi untuk Integritas Data

Operasi yang memodifikasi beberapa tabel memerlukan transaksi. TypeORM menawarkan dua pendekatan: method DataSource.transaction() dan API QueryRunner.

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);
  });
}

Method transaction() berbasis callback secara otomatis commit pada keberhasilan dan rollback pada error yang dilemparkan. Setiap operasi database di dalam callback harus menggunakan manager yang disediakan, bukan repository yang diinjeksi, karena operasi tersebut berjalan di luar transaksi.

QueryRunner vs transaction()

Gunakan DataSource.transaction() untuk operasi multi-langkah yang sederhana. Beralih ke QueryRunner ketika kontrol manual atas commit/rollback point atau savepoint diperlukan, seperti partial rollback dalam batch processing.

Pertanyaan Wawancara NestJS TypeORM yang Umum

Pertanyaan-pertanyaan ini sering muncul dalam wawancara backend developer. Setiap jawaban menargetkan kedalaman yang diharapkan dalam diskusi teknis tingkat senior.

Mengapa synchronize: true tidak boleh digunakan di produksi?

Auto-sync menerapkan perubahan skema yang destruktif tanpa konfirmasi. Menghapus kolom, mengubah tipe, atau menghapus indeks terjadi langsung terhadap data live. Migrasi menyediakan version control, code review, dan kemampuan rollback. Satu-satunya penggunaan synchronize yang aman adalah pengembangan lokal dengan data yang dapat dibuang.

Apa perbedaan antara relasi eager dan lazy?

Relasi eager dimuat secara otomatis dengan setiap query pada entitas induk. Relasi lazy mengembalikan Promise dan mengeksekusi query terpisah hanya ketika diakses. Eager loading menyebabkan overhead gaya N+1 ketika data terkait tidak diperlukan. Lazy loading menunda biaya tetapi dapat memicu query tak terduga jauh di dalam call stack. Opsi relations eksplisit dalam find() atau leftJoinAndSelect dalam QueryBuilder memberikan kontrol penuh atas apa yang dimuat per query.

Bagaimana cascade bekerja dalam relasi TypeORM?

Mengatur cascade: true pada relasi berarti menyimpan entitas induk juga mempersist entitas anak yang belum disimpan. cascade: ['insert', 'update'] membatasi perilaku ke operasi tertentu. Cascade beroperasi di level ORM, bukan level database. Untuk cascading delete di level database, gunakan onDelete: 'CASCADE' pada decorator @ManyToOne, yang menghasilkan constraint ON DELETE CASCADE dalam migrasi.

Kapan custom repository harus digunakan daripada Repository<T> default?

Custom repository mengenkapsulasi logika query kompleks yang tidak termasuk dalam service. Tanda-tanda untuk mengekstrak custom repository: chain QueryBuilder yang sama muncul di beberapa service, query melibatkan beberapa join atau subquery, atau query memerlukan raw SQL. Panggilan find() sederhana tidak membenarkan custom repository.

Untuk pertanyaan arsitektur NestJS lainnya, lihat panduan lengkap guards dan interceptors NestJS.

Siap menguasai wawancara Node.js / NestJS Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Sumber

Poin Penting untuk Aplikasi NestJS TypeORM Produksi

  • Gunakan file konfigurasi DataSource terpisah untuk CLI TypeORM, independen dari konfigurasi modul NestJS
  • Jangan pernah mengaktifkan synchronize di produksi; andalkan migrasi yang dihasilkan dan ditinjau untuk semua perubahan skema
  • Tempatkan @ManyToOne pada entitas yang memiliki foreign key, dan gunakan penamaan @JoinColumn eksplisit untuk kejelasan dalam raw query dan output migrasi
  • Pilih antara options API find() untuk query sederhana dan QueryBuilder untuk join kompleks, agregasi, dan logika kondisional
  • Bungkus mutasi multi-tabel dalam transaksi menggunakan DataSource.transaction(), dan pastikan setiap operasi di dalam callback menggunakan manager transaksional
  • Dengan TypeORM 1.1.x yang menstabilkan API surface dan NestJS 12 yang menyelesaikan migrasi ESM-nya, stack ini tetap menjadi pilihan kuat untuk backend enterprise yang memerlukan akses database yang terstruktur dan type-safe

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tantangan harian

Bisakah kamu menemukan bug di Node.js / NestJS?

Satu potongan kode nyata, satu bug tersembunyi, satu percobaan per hari. Tanpa akun untuk mencoba.

Anthony Fillion-Maillet

Ditulis oleh

Anthony Fillion-Maillet

Pendiri SharpSkill

Developer fullstack selama lebih dari 10 tahun. Ia menjalankan SharpSkill dan bertanggung jawab atas semua yang diterbitkan di sini.

Diperbarui 21 Oktober 2026

Tag

#nestjs
#typeorm
#migrations
#database
#typescript
#backend

Bagikan

Artikel terkait