# NestJS và TypeORM năm 2026: Migration, Relation và Câu hỏi Phỏng vấn > Hướng dẫn toàn diện về NestJS kết hợp TypeORM trong năm 2026: cấu hình DataSource, migration, các loại relation (OneToMany, ManyToMany), Repository pattern, transaction và câu hỏi phỏng vấn kỹ thuật. - Published: 2026-06-19 - Updated: 2026-06-19 - Author: SharpSkill - Reading time: 5 min --- NestJS và TypeORM tiếp tục là một trong những bộ đôi được sử dụng rộng rãi nhất trong hệ sinh thái Node.js backend năm 2026. NestJS mang lại kiến trúc module hóa với Dependency Injection mạnh mẽ, trong khi TypeORM cung cấp công cụ thao tác cơ sở dữ liệu an toàn kiểu dữ liệu bằng TypeScript. Tuy nhiên, nhiều dự án gặp khó khăn khi thiết lập migration đúng cách, mô hình hóa các mối quan hệ phức tạp giữa entity, và đảm bảo tính nhất quán dữ liệu trong các thao tác nhiều bước. Bài viết này trình bày các khái niệm cốt lõi về cấu hình TypeORM trong dự án NestJS, quy trình migration chuyên nghiệp, các loại relation phổ biến, Repository pattern và transaction. Phần cuối bao gồm những câu hỏi thường xuất hiện trong phỏng vấn kỹ thuật dành cho lập trình viên backend. > **API DataSource của TypeORM** > > TypeORM đã thay đổi cơ bản API cấu hình. Các dự án còn sử dụng `ormconfig.json` hoặc `createConnection` cần chuyển sang API `DataSource`. Tất cả ví dụ trong bài viết này sử dụng API hiện tại của TypeORM. ## Cấu hình TypeORM trong dự án NestJS Bước đầu tiên là tạo một file cấu hình DataSource độc lập. File này được TypeORM CLI sử dụng để thực hiện các thao tác migration. Việc tách biệt cấu hình đảm bảo CLI có thể hoạt động độc lập với ngữ cảnh NestJS. ```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, }); ``` Thuộc tính `synchronize: false` cần được đặc biệt lưu ý. Trong môi trường phát triển, việc đặt `synchronize: true` có vẻ tiện lợi vì TypeORM tự động đồng bộ schema cơ sở dữ liệu với các entity. Tuy nhiên, trong môi trường production, điều này có thể dẫn đến mất dữ liệu và các hành vi không dự đoán trước. Thay vào đó, migration là phương pháp chuẩn để quản lý thay đổi schema. Trong `AppModule`, TypeORM được tích hợp thông qua cấu hình bất đồng bộ, trong đó `ConfigService` cung cấp các biến môi trường: ```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, synchronize: false, }), }), ], }) export class AppModule {} ``` Sự khác biệt giữa `config.get()` và `config.getOrThrow()` tuy tinh tế nhưng có ý nghĩa thực tiễn quan trọng. Phương thức `getOrThrow()` ném exception khi biến môi trường không tồn tại. Đối với các giá trị cấu hình quan trọng như port cơ sở dữ liệu, điều này đảm bảo lỗi cấu hình được phát hiện ngay lập tức thay vì sử dụng giá trị mặc định một cách âm thầm. ## Migration: Tạo, thực thi và hoàn tác Migration là nền tảng của mọi chiến lược quản lý cơ sở dữ liệu chuyên nghiệp. Chúng ghi lại các thay đổi schema theo phiên bản, có tính tái tạo và có thể hoàn tác. Các npm script sau giúp đơn giản hóa việc tương tác với TypeORM CLI: ```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" } ``` Quy trình làm việc trong thực tế diễn ra như sau: trước tiên, cập nhật các entity tương ứng. Sau đó, lệnh `npm run migration:generate --name=AddUserTable` tự động tạo một file migration chứa sự khác biệt giữa schema hiện tại và định nghĩa entity. Lệnh `npm run migration:run` áp dụng migration vào cơ sở dữ liệu, và `npm run migration:revert` hoàn tác migration gần nhất. Một lỗi thường gặp trong các dự án là viết migration thủ công khi `migration:generate` đã tự động hóa việc so sánh schema. Migration thủ công chỉ cần thiết khi cần chuyển đổi dữ liệu, chẳng hạn khi đổi tên cột kèm theo việc di chuyển dữ liệu từ cột cũ sang cột mới. > **Luôn kiểm tra migration được tạo tự động** > > Khi đổi tên một cột, TypeORM tạo lệnh DROP + ADD thay vì ALTER RENAME, điều này sẽ xóa toàn bộ dữ liệu trong cột đó. Các câu lệnh SQL trong phương thức `up()` cần được kiểm tra kỹ lưỡng và điều chỉnh thủ công khi cần thiết. ## Entity và các loại Relation TypeORM sử dụng decorator để ánh xạ cấu trúc cơ sở dữ liệu trực tiếp vào các lớp TypeScript. Ba loại relation quan trọng nhất bao gồm OneToMany/ManyToOne, ManyToMany và OneToOne. ### OneToMany và ManyToOne Mối quan hệ cổ điển giữa người dùng và đơn hàng minh họa cách `@OneToMany` và `@ManyToOne` phối hợp với nhau: ```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[]; @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' }) user: User; } ``` Điểm then chốt ở đây là: phía `@OneToMany` không tạo bất kỳ cột nào trong cơ sở dữ liệu. Khóa ngoại luôn nằm ở phía `@ManyToOne`. Decorator `@JoinColumn` chỉ định rõ tên cột khóa ngoại, giúp tăng tính dễ đọc của cơ sở dữ liệu và tránh xung đột tên. Tùy chọn `onDelete: 'CASCADE'` đảm bảo các đơn hàng tự động bị xóa khi người dùng tương ứng bị loại bỏ khỏi hệ thống. ### ManyToMany Đối với sản phẩm và danh mục, quan hệ ManyToMany là lựa chọn phù hợp. TypeORM tự động tạo bảng trung gian (junction table): ```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' }) categories: Category[]; } ``` Decorator `@JoinTable` đánh dấu phía sở hữu (owning side) của relation. Chỉ một phía được phép sử dụng decorator này. Trong trường hợp bảng trung gian cần chứa thêm các cột bổ sung (ví dụ thứ tự sắp xếp), cần tạo một entity riêng với hai relation `@ManyToOne` thay vì sử dụng `@ManyToMany` trực tiếp. ## Repository Pattern và Feature Module NestJS khuyến khích việc module hóa thông qua feature module. Mỗi module đăng ký các entity của mình thông qua `TypeOrmModule.forFeature()` và từ đó có quyền truy cập vào các repository tương ứng: ```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 {} ``` Trong service, repository được inject thông qua `@InjectRepository`. Đối với các truy vấn đơn giản, Repository API là đủ. Khi gặp các tình huống phức tạp hơn, QueryBuilder sẽ được sử dụng: ```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'], order: { createdAt: 'DESC' }, }); } async findWithFilters(status: string, minTotal: number): Promise { return this.orderRepo .createQueryBuilder('order') .leftJoinAndSelect('order.user', 'user') .where('order.status = :status', { status }) .andWhere('order.total >= :minTotal', { minTotal }) .orderBy('order.total', 'DESC') .getMany(); } } ``` Phương thức `findByUser` minh họa cách lọc relation thông qua các điều kiện Where lồng nhau. Mảng `relations` kiểm soát những liên kết nào được tải cùng. Nếu không chỉ định, TypeORM chỉ trả về các đơn hàng mà không kèm theo dữ liệu người dùng. QueryBuilder trong `findWithFilters` cung cấp nhiều quyền kiểm soát hơn: `leftJoinAndSelect` tải relation và hiển thị trong kết quả, trong khi các truy vấn tham số hóa ngăn chặn SQL Injection một cách đáng tin cậy. ## Transaction: Đảm bảo tính nhất quán dữ liệu Khi nhiều thao tác cơ sở dữ liệu cần diễn ra một cách nguyên tử (atomic), transaction là giải pháp bắt buộc. TypeORM cung cấp phương thức `DataSource.transaction()` cho mục đích này: ```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) => { 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); }); } ``` Bên trong transaction, tất cả các thao tác đều phải sử dụng `manager` được cung cấp. Nếu sử dụng repository thông thường, các thao tác đó sẽ chạy bên ngoài transaction và khi xảy ra lỗi ở một bước sau, dữ liệu không thể được hoàn tác, dẫn đến trạng thái không nhất quán. Khi bất kỳ lỗi nào xảy ra bên trong callback, toàn bộ transaction sẽ tự động được rollback. > **QueryRunner và transaction()** > > `DataSource.transaction()` phù hợp cho các thao tác nhiều bước đơn giản. `QueryRunner` cung cấp khả năng kiểm soát thủ công các điểm Commit/Rollback và Savepoint, hữu ích trong các trường hợp cần rollback từng phần khi xử lý hàng loạt (batch processing). ## Câu hỏi phỏng vấn thường gặp về NestJS và TypeORM Các buổi phỏng vấn kỹ thuật về NestJS và TypeORM thường tập trung vào ba lĩnh vực: hiểu biết kiến trúc, khả năng giải quyết vấn đề thực tế và khả năng tránh các lỗi phổ biến. **Tại sao cần tắt `synchronize` trong môi trường production?** Tùy chọn `synchronize: true` tự động đồng bộ schema cơ sở dữ liệu với định nghĩa entity mỗi khi ứng dụng khởi động. Điều này có thể xóa cột, thay đổi kiểu dữ liệu hoặc loại bỏ bảng mà không có bất kỳ ghi chép nào. Trong môi trường production, hậu quả là mất dữ liệu. Migration cung cấp giải pháp thay thế: các thay đổi schema được phiên bản hóa, có tính tái tạo và có thể hoàn tác. **Sự khác biệt giữa `migration:generate` và `migration:create` là gì?** Lệnh `migration:generate` so sánh định nghĩa entity hiện tại với schema cơ sở dữ liệu và tự động tạo ra các câu lệnh SQL cần thiết. Lệnh `migration:create` tạo một file migration trống, trong đó các câu lệnh SQL được viết thủ công. Trường hợp thứ hai hữu ích cho các thao tác chuyển đổi dữ liệu hoặc seed dữ liệu. **Khóa ngoại nằm ở phía nào trong quan hệ OneToMany/ManyToOne?** Khóa ngoại luôn nằm ở phía `@ManyToOne`. Phía `@OneToMany` không tạo bất kỳ cột nào trong cơ sở dữ liệu -- nó chỉ phục vụ cho việc biểu diễn mối quan hệ ngược (inverse relation) trong TypeScript. **Khi nào nên sử dụng QueryBuilder thay vì Repository API?** Repository API (`find`, `findOne`, `save`) phù hợp cho các thao tác CRUD đơn giản và truy vấn với điều kiện lọc trực tiếp. QueryBuilder được sử dụng khi cần các phép JOIN phức tạp, subquery, hàm tổng hợp hoặc các truy vấn được xây dựng động tại runtime. **Làm thế nào để giải quyết Circular Dependency giữa các module?** NestJS cung cấp `forwardRef()` để xử lý các phụ thuộc vòng giữa các module. Tuy nhiên, việc sử dụng nó thường là dấu hiệu của vấn đề kiến trúc. Giải pháp tốt hơn là trích xuất logic dùng chung vào một module riêng biệt. **Điều gì xảy ra khi sử dụng repository thông thường thay vì manager trong một transaction?** Các thao tác thông qua repository thông thường sẽ chạy bên ngoài transaction. Điều này có nghĩa là những thay đổi đó được commit ngay lập tức và không thể rollback khi có lỗi xảy ra. Kết quả là dữ liệu rơi vào trạng thái không nhất quán. ## Kết luận Sự kết hợp giữa NestJS và TypeORM mang lại một hệ sinh thái toàn diện cho việc phát triển backend an toàn kiểu dữ liệu với Node.js. Những điểm chính có thể tổng kết như sau: - **Migration** thay thế `synchronize` trong mọi môi trường ngoài phát triển cục bộ - **Relation** đòi hỏi sự hiểu biết rõ ràng về phía nào sở hữu khóa ngoại - **Transaction** phải sử dụng `manager` được cung cấp, không bao giờ dùng repository thông thường - **QueryBuilder** bổ sung cho Repository API tại những nơi các phương thức đơn giản không đáp ứng được - Trong phỏng vấn kỹ thuật, không chỉ cú pháp mà lý do đằng sau các quyết định kiến trúc mới là yếu tố quyết định --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/node-nestjs/nestjs-typeorm-migrations-relations-best-practices