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

Panduan lengkap NestJS dan TypeORM 1.0: konfigurasi migrasi, relasi OneToMany dan ManyToMany, repository pattern, manajemen transaksi, dan pertanyaan wawancara backend developer.

NestJS dan TypeORM: Migrasi, Relasi, dan Pertanyaan Wawancara

Dalam pengembangan backend modern dengan Node.js, kombinasi NestJS dan TypeORM telah membuktikan diri sebagai salah satu stack paling andal untuk membangun aplikasi yang type-safe, terstruktur, dan siap produksi. NestJS menghadirkan arsitektur modular berbasis decorator dan dependency injection yang terinspirasi dari Angular, sedangkan TypeORM menjembatani dunia TypeScript dengan database relasional melalui pemetaan objek-relasional yang intuitif. Dengan dirilisnya TypeORM 1.0 pada Mei 2026 yang membawa stabilitas API DataSource dan perbaikan performa signifikan, serta NestJS 11 yang terus menyempurnakan integrasi database, tahun 2026 menjadi momen yang tepat untuk mendalami cara kerja migrasi, relasi antar-entitas, dan pola-pola yang kerap diujikan dalam wawancara teknis backend.

Artikel ini menyajikan panduan praktis mulai dari konfigurasi awal TypeORM dalam proyek NestJS, pengelolaan migrasi database secara profesional, pemodelan berbagai tipe relasi, pemanfaatan repository pattern, hingga manajemen transaksi. Setiap bagian dilengkapi contoh kode yang dapat langsung diterapkan, serta ditutup dengan pertanyaan wawancara yang sering muncul di perusahaan teknologi.

TypeORM 1.0 dan API DataSource Baru

TypeORM 1.0, yang dirilis Mei 2026, secara resmi menghapus dukungan untuk ormconfig.json dan createConnection. Seluruh konfigurasi kini dilakukan melalui API DataSource yang lebih eksplisit dan type-safe. Proyek yang masih menggunakan pendekatan lama perlu segera bermigrasi. Semua contoh dalam artikel ini sudah sepenuhnya mengadopsi API terbaru tersebut.

Konfigurasi DataSource dan Integrasi dengan NestJS

Sebelum dapat menjalankan migrasi atau mendefinisikan entitas, proyek memerlukan dua file konfigurasi terpisah. File pertama adalah konfigurasi DataSource mandiri yang digunakan oleh CLI TypeORM. File ini beroperasi di luar konteks NestJS, sehingga CLI dapat menghasilkan dan menjalankan migrasi tanpa perlu memulai seluruh aplikasi.

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

Pengaturan synchronize: false bukan sekadar rekomendasi, melainkan keharusan mutlak untuk setiap lingkungan selain pengembangan lokal yang bersifat eksperimental. Ketika synchronize diaktifkan, TypeORM secara diam-diam menyesuaikan skema database setiap kali aplikasi dimulai ulang. Proses otomatis ini dapat menghapus kolom yang berisi data, mengubah constraint, atau bahkan menjatuhkan tabel secara keseluruhan tanpa meninggalkan jejak audit. Migrasi menjadi satu-satunya mekanisme yang memberikan kontrol penuh, dokumentasi perubahan, dan kemampuan rollback.

File kedua adalah konfigurasi TypeORM di dalam AppModule NestJS, yang memanfaatkan ConfigService untuk membaca variabel lingkungan secara aman:

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

Terdapat perbedaan penting antara config.get() dan config.getOrThrow() yang perlu dicermati. Method getOrThrow() akan segera melemparkan exception apabila variabel lingkungan yang diminta tidak tersedia. Untuk parameter kritis seperti port database, perilaku ini jauh lebih aman dibandingkan menerima undefined secara diam-diam yang baru menimbulkan error saat koneksi database gagal di tengah runtime. Opsi autoLoadEntities: true mengeliminasi kebutuhan untuk mendaftarkan setiap entitas secara manual di konfigurasi root, karena NestJS secara otomatis mengenali entitas yang diregistrasikan melalui TypeOrmModule.forFeature() pada modul-modul fitur.

Alur Kerja Migrasi: Generate, Review, Run

Migrasi database merupakan fondasi dari pengelolaan skema yang profesional dan dapat dipertanggungjawabkan. Setiap perubahan struktur database tercatat dalam file yang berversi, dapat dijalankan secara berulang, dan dapat dikembalikan ke kondisi sebelumnya. Langkah pertama adalah menyiapkan npm script yang menyederhanakan interaksi dengan CLI TypeORM:

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

Alur kerja standar dalam praktik sehari-hari dimulai dari modifikasi file entitas sesuai kebutuhan bisnis. Setelah perubahan entitas disimpan, perintah npm run migration:generate --name=CreateOrdersTable dijalankan. TypeORM akan membandingkan definisi entitas terkini dengan skema database yang sedang berjalan, kemudian menghasilkan file migrasi berisi statement SQL yang merepresentasikan perbedaan tersebut. Perintah npm run migration:run menerapkan seluruh migrasi yang belum dieksekusi ke database, sedangkan npm run migration:revert membatalkan migrasi terakhir yang diterapkan.

Kesalahan yang kerap dijumpai pada banyak tim adalah menuliskan file migrasi secara manual dari awal. Pendekatan ini tidak hanya membuang waktu, tetapi juga rawan kesalahan karena developer harus secara manual melacak perbedaan antara definisi entitas dan skema database. Perintah migration:generate menangani seluruh proses perbandingan secara otomatis. Penulisan migrasi manual hanya diperlukan dalam skenario khusus, misalnya ketika data pada kolom yang sudah ada perlu ditransformasikan atau dipindahkan ke struktur baru.

Selalu Review File Migrasi Sebelum Dijalankan

Perintah migration:generate tidak selalu menghasilkan SQL yang optimal. Contoh paling umum: ketika nama kolom diubah pada entitas, TypeORM menghasilkan perintah DROP diikuti ADD, bukan ALTER RENAME. Akibatnya, seluruh data pada kolom tersebut hilang secara permanen. Method up() pada file migrasi yang dihasilkan wajib diperiksa secara manual dan disesuaikan jika diperlukan sebelum dijalankan ke database mana pun.

Pemodelan Relasi Antar-Entitas

TypeORM memanfaatkan sistem decorator untuk memetakan struktur tabel database ke dalam class TypeScript secara deklaratif. Tiga jenis relasi yang paling sering diimplementasikan dalam aplikasi backend adalah OneToMany/ManyToOne, ManyToMany, dan OneToOne. Pemahaman mendalam tentang di sisi mana foreign key ditempatkan dan bagaimana masing-masing decorator bekerja menjadi kompetensi yang sangat dihargai dalam wawancara teknis.

Relasi OneToMany dan ManyToOne

Hubungan antara pengguna dan pesanan merupakan contoh klasik relasi OneToMany/ManyToOne. Seorang pengguna dapat memiliki banyak pesanan, sementara setiap pesanan hanya dimiliki oleh satu pengguna. Berikut definisi kedua entitas:

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

Aspek yang paling sering menimbulkan kebingungan pada developer yang baru menggunakan TypeORM adalah lokasi penyimpanan foreign key. Properti orders pada entitas User yang didekorasi dengan @OneToMany tidak menghasilkan kolom apa pun di tabel users. Foreign key (user_id) selalu berada di sisi @ManyToOne, yaitu pada tabel orders. Decorator @JoinColumn({ name: 'user_id' }) secara eksplisit menentukan nama kolom foreign key, menghindari penamaan otomatis TypeORM yang terkadang kurang deskriptif. Opsi onDelete: 'CASCADE' memastikan bahwa ketika sebuah record pengguna dihapus, seluruh pesanan terkait ikut terhapus secara otomatis di level database.

Relasi ManyToMany

Relasi antara produk dan kategori menggambarkan skenario di mana satu produk dapat termasuk dalam banyak kategori, dan satu kategori dapat memuat banyak produk. TypeORM menangani kompleksitas ini dengan secara otomatis membuat tabel penghubung (junction table).

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

Decorator @JoinTable menandai sisi pemilik (owning side) dari relasi ManyToMany. Hanya satu sisi yang boleh memiliki decorator ini, dan sisi tersebut yang bertanggung jawab atas pengelolaan tabel penghubung. Pada contoh di atas, tabel product_categories dibuat secara otomatis dengan dua kolom foreign key yang mereferensikan tabel products dan tabel categories.

Apabila tabel penghubung memerlukan kolom tambahan, misalnya timestamp kapan produk ditambahkan ke kategori atau urutan sortir, pendekatan @ManyToMany dengan @JoinTable tidak lagi memadai. Solusi yang tepat adalah membuat entitas perantara secara eksplisit dengan dua relasi @ManyToOne yang masing-masing menghubungkan ke entitas Product dan Category. Pola ini memberikan kontrol penuh atas data di tabel penghubung.

Untuk pembahasan lebih lanjut mengenai pola-pola database di NestJS, silakan merujuk ke modul database NestJS.

Siap menguasai wawancara Node.js / NestJS Anda?

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

Repository Pattern dan Organisasi Modul Fitur

Arsitektur modul NestJS mendorong pemisahan tanggung jawab melalui konsep modul fitur. Setiap domain bisnis dienkapsulasi dalam modulnya sendiri, yang mendaftarkan entitas terkait melalui TypeOrmModule.forFeature(). Pendekatan ini memastikan bahwa setiap modul hanya memiliki akses ke repository yang dibutuhkannya.

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

Di dalam service layer, repository diinjeksikan menggunakan decorator @InjectRepository. TypeORM menyediakan dua pendekatan untuk mengeksekusi query: Repository API untuk operasi sederhana, dan QueryBuilder untuk skenario yang memerlukan kontrol lebih mendetail terhadap SQL yang dihasilkan.

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

Method findByUser memperlihatkan cara memfilter berdasarkan relasi menggunakan objek where bersarang. Array relations pada opsi find mengontrol relasi mana yang ikut dimuat bersama hasil query. Tanpa pengaturan ini, TypeORM hanya mengembalikan data pesanan tanpa informasi pengguna yang terkait, dan akses ke properti order.user akan menghasilkan undefined.

QueryBuilder pada method findWithFilters menawarkan fleksibilitas yang jauh lebih tinggi. Perintah leftJoinAndSelect melakukan join sekaligus memasukkan data relasi ke dalam hasil query. Penggunaan parameter bernama (:status, :minTotal) bukan sekadar gaya penulisan yang lebih rapi, melainkan mekanisme perlindungan terhadap SQL injection. TypeORM secara otomatis melakukan escaping pada nilai parameter sebelum menyuntikkannya ke dalam query.

Untuk memahami bagaimana repository pattern berinteraksi dengan sistem modul dan DI NestJS, pemahaman tentang scope injection dan lifecycle provider juga sangat diperlukan.

Manajemen Transaksi untuk Konsistensi Data

Operasi bisnis yang melibatkan lebih dari satu tabel sering kali memerlukan jaminan atomisitas: semua langkah berhasil, atau tidak ada yang diterapkan sama sekali. Contoh klasik adalah pembuatan pesanan yang disertai penyimpanan detail item pesanan dan perhitungan total harga. Apabila salah satu langkah gagal di tengah proses tanpa mekanisme transaksi, database akan berisi data yang tidak konsisten, misalnya pesanan tanpa item atau total harga yang tidak sesuai dengan item yang tercatat.

TypeORM menyediakan method DataSource.transaction() yang membungkus serangkaian operasi dalam satu unit transaksional:

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

Prinsip fundamental yang harus dipatuhi di dalam blok transaksi: seluruh operasi database wajib dilakukan melalui objek manager yang disediakan oleh callback. Penggunaan repository reguler (this.orderRepo) di dalam transaksi merupakan kesalahan serius karena operasi melalui repository reguler berjalan di luar cakupan transaksi tersebut. Perubahan yang dilakukan melalui repository reguler langsung di-commit ke database, sehingga tidak dapat di-rollback ketika langkah berikutnya gagal.

Apabila terjadi exception pada langkah mana pun di dalam callback, TypeORM secara otomatis melakukan rollback seluruh transaksi, memastikan database kembali ke kondisi sebelum transaksi dimulai.

QueryRunner vs DataSource.transaction()

Method DataSource.transaction() merupakan pilihan yang tepat untuk operasi multi-langkah standar karena TypeORM menangani siklus hidup koneksi, commit, dan rollback secara otomatis. Untuk skenario yang memerlukan kontrol manual lebih granular, seperti penggunaan savepoint dalam pemrosesan batch atau partial rollback pada serangkaian operasi yang sebagian boleh gagal, QueryRunner menyediakan method startTransaction(), commitTransaction(), rollbackTransaction(), dan release() yang dapat dipanggil secara eksplisit.

Pertanyaan Wawancara Teknis NestJS dan TypeORM

Wawancara teknis untuk posisi backend engineer yang bekerja dengan NestJS dan TypeORM umumnya menggali tiga kompetensi utama: pemahaman arsitektur dan alasan di balik keputusan desain, kemampuan mengidentifikasi dan menyelesaikan masalah praktis, serta penguasaan best practices untuk lingkungan produksi.

Mengapa opsi synchronize wajib dinonaktifkan di lingkungan produksi?

Opsi synchronize: true menginstruksikan TypeORM untuk menyesuaikan skema database secara otomatis berdasarkan definisi entitas setiap kali aplikasi dijalankan. Dalam lingkungan produksi, perilaku ini berpotensi menghapus kolom yang berisi data, mengubah tipe data tanpa proses migrasi, atau menjatuhkan constraint yang melindungi integritas data. Tidak ada log atau catatan perubahan yang dihasilkan, sehingga debugging menjadi sangat sulit. Migrasi menyediakan alternatif yang aman karena setiap perubahan skema memiliki file tersendiri yang dapat ditinjau, dijalankan secara terkontrol, dan dikembalikan ke versi sebelumnya.

Apa perbedaan antara migration:generate dan migration:create?

Perintah migration:generate secara otomatis membandingkan definisi entitas saat ini dengan skema database yang aktif, kemudian menghasilkan file migrasi berisi SQL yang merepresentasikan perbedaan di antara keduanya. Perintah migration:create hanya membuat file migrasi kosong tanpa isi apa pun, yang kemudian diisi oleh developer secara manual. Penggunaan migration:create tepat untuk situasi di mana perubahan tidak terdeteksi secara otomatis, seperti operasi seed data, penggantian nama kolom dengan preservasi data, atau penambahan indeks parsial dengan kondisi khusus.

Di sisi mana foreign key berada pada relasi OneToMany/ManyToOne?

Foreign key selalu berada di sisi @ManyToOne, yaitu di tabel yang merepresentasikan sisi "banyak" dari relasi. Sisi @OneToMany tidak menghasilkan kolom apa pun di database dan hanya berfungsi sebagai representasi navigasi balik di level kode TypeScript. Decorator @JoinColumn ditempatkan di sisi @ManyToOne untuk menentukan nama kolom foreign key secara eksplisit.

Kapan QueryBuilder lebih tepat digunakan dibandingkan Repository API?

Repository API (find, findOne, save, remove) sangat memadai untuk operasi CRUD standar dan query dengan kondisi filter langsung. QueryBuilder menjadi pilihan yang lebih tepat ketika query melibatkan join antar beberapa tabel, subquery, fungsi agregasi seperti SUM atau COUNT, kondisi WHERE yang dibangun secara dinamis berdasarkan input pengguna, atau penggunaan HAVING dan GROUP BY. Prinsipnya sederhana: mulai dengan Repository API, dan beralih ke QueryBuilder hanya ketika Repository API tidak mampu mengekspresikan query yang dibutuhkan.

Apa risiko menggunakan repository reguler di dalam blok transaksi, bukan manager?

Operasi yang dieksekusi melalui repository reguler berjalan di luar cakupan transaksi. Artinya, perubahan tersebut langsung di-commit ke database secara independen. Apabila langkah berikutnya dalam transaksi mengalami kegagalan dan transaksi di-rollback, perubahan yang sudah di-commit melalui repository reguler tidak ikut dibatalkan. Kondisi ini menghasilkan data yang tidak konsisten, di mana sebagian operasi berhasil dan sebagian lainnya gagal, dan seringkali sulit dideteksi hingga terjadi masalah di level bisnis.

Bagaimana cara mengatasi circular dependency antar-modul di NestJS?

NestJS menyediakan fungsi forwardRef() yang memungkinkan modul mereferensikan satu sama lain secara sirkular. Namun, penggunaan forwardRef() seringkali merupakan indikasi bahwa arsitektur modul perlu ditinjau ulang. Solusi yang lebih baik adalah mengekstraksi logika yang digunakan bersama ke dalam modul ketiga yang bersifat independen, kemudian kedua modul asli mengimpor modul baru tersebut tanpa menciptakan ketergantungan sirkular. Penjelasan lebih lengkap tentang strategi ini dapat dipelajari pada panduan guards dan interceptors NestJS.

Bagaimana cara mengoptimalkan query yang memuat banyak relasi sekaligus?

Loading seluruh relasi secara default melalui opsi eager: true pada decorator relasi merupakan anti-pattern karena setiap query akan memuat data yang mungkin tidak diperlukan. Pendekatan yang lebih baik adalah menggunakan lazy loading melalui opsi relations pada method find, atau leftJoinAndSelect pada QueryBuilder, sehingga hanya relasi yang benar-benar dibutuhkan oleh endpoint tertentu yang dimuat. Untuk endpoint dengan traffic tinggi, pertimbangkan penggunaan select pada QueryBuilder untuk membatasi kolom yang diambil dari setiap tabel yang di-join.

Kesimpulan

NestJS dan TypeORM bersama-sama menyediakan ekosistem yang matang dan teruji untuk membangun backend Node.js yang type-safe, modular, dan siap menghadapi kebutuhan produksi. Berikut rangkuman poin-poin kunci dari seluruh pembahasan:

  • Konfigurasi DataSource terpisah diperlukan agar CLI TypeORM dapat beroperasi secara independen dari konteks NestJS, sementara TypeOrmModule.forRootAsync menangani integrasi di sisi aplikasi dengan dukungan ConfigService untuk keamanan konfigurasi.
  • Migrasi menggantikan synchronize di setiap lingkungan selain pengembangan lokal. Alur generate-review-run memastikan setiap perubahan skema terdokumentasi, dapat dilacak, dan dapat dikembalikan.
  • Relasi memerlukan pemahaman yang jelas tentang penempatan foreign key: selalu di sisi @ManyToOne untuk OneToMany, dan melalui @JoinTable di sisi pemilik untuk ManyToMany.
  • Repository pattern menyediakan API yang bersih untuk operasi CRUD standar, sementara QueryBuilder melengkapi kebutuhan query kompleks dengan perlindungan bawaan terhadap SQL injection melalui parameterisasi.
  • Transaksi harus secara konsisten menggunakan objek manager dari callback, bukan repository reguler, untuk menjamin bahwa seluruh operasi bersifat atomik dan dapat di-rollback secara utuh.
  • Dalam wawancara teknis, kemampuan menjelaskan alasan di balik keputusan arsitektur dinilai sama pentingnya dengan kemampuan menulis kode yang benar secara sintaksis.

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#nestjs
#typeorm
#database
#migrations
#best-practices

Bagikan

Artikel terkait