NestJS và TypeORM năm 2026: Migration, Relation và Câu hỏi Phỏng vấn
Thành thạo tích hợp NestJS TypeORM với migration TypeORM 1.1, entity relation, repository pattern và câu hỏi phỏng vấn thường gặp cho backend developer.

NestJS TypeORM vẫn là một trong những tích hợp ORM được kiểm chứng nhất cho backend Node.js doanh nghiệp. Với TypeORM đạt phiên bản 1.0 vào tháng 5 năm 2026, tiếp theo là loạt bản vá 1.1.x giải quyết xử lý kết nối và lưu trữ relation, cùng với NestJS 12 hoàn thành quá trình chuyển đổi ESM, stack này đã trưởng thành thành nền tảng ổn định cho các ứng dụng production. Hướng dẫn này đề cập các pattern thiết yếu mà mọi backend developer cần thành thạo: migration, entity relation, repository pattern và các câu hỏi phỏng vấn đi kèm.
NestJS 12 cung cấp các gói core dưới dạng ESM, yêu cầu Node.js 20.19+ hoặc 22.12+. Gói @nestjs/typeorm v12 cũng tương tự. TypeORM 1.1.2 sửa lỗi rò rỉ kết nối trong DataSource.initialize() và khắc phục việc lưu trữ junction row many-to-many khi relation không được load.
Thiết lập Migration TypeORM trong NestJS 12
Quy tắc đầu tiên của database production: không bao giờ sử dụng synchronize: true. Auto-sync so sánh metadata entity với schema đang chạy và áp dụng thay đổi trực tiếp, có nguy cơ mất dữ liệu khi xóa cột, thay đổi kiểu, hoặc đổi tên field. Migration cung cấp giải pháp thay thế có phiên bản, có thể xem xét và có thể hoàn tác.
TypeORM 1.x yêu cầu file cấu hình DataSource riêng cho CLI, tách biệt với cấu hình module NestJS. CLI không thể resolve dependency injection của NestJS, nên cần một entry point độc lập.
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,
});Cấu hình module NestJS phản ánh các thiết lập này nhưng sử dụng pattern TypeOrmModule.forRootAsync với dependency injection:
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 thay thế mảng entities thủ công bằng cách tự động đăng ký mọi entity được import qua TypeOrmModule.forFeature() trong các feature module.
Quy trình Migration: Generate, Review, Run
TypeORM 1.x cung cấp typeorm-ts-node-commonjs là CLI runner được khuyến nghị, thay thế cách tiếp cận cũ ts-node ./node_modules/typeorm/cli. Thêm các script sau vào package.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"
}Lệnh generate so sánh metadata entity với database đang chạy và tạo ra các câu lệnh SQL. Luôn xem xét output trước khi commit. TypeORM phát hiện việc thêm cột, xóa, thay đổi kiểu, sửa đổi index và thay đổi foreign key, nhưng bỏ lỡ backfill dữ liệu, đổi tên bảng (xuất hiện dưới dạng drop rồi create), và mọi thứ liên quan đến database extension hoặc trigger.
Cột được đổi tên sẽ tạo ra DROP + ADD thay vì ALTER RENAME. Điều này phá hủy dữ liệu hiện có. Luôn kiểm tra SQL trong method up() và điều chỉnh thủ công khi cần.
Entity Relation: OneToMany, ManyToOne, ManyToMany
Các relation entity TypeORM ánh xạ trực tiếp sang foreign key SQL bằng decorator. Hiểu ba loại relation cốt lõi và các tùy chọn của chúng là nền tảng cho mọi module database NestJS.
User với nhiều bản ghi Order minh họa pattern phổ biến nhất:
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;
}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;
}Phía @ManyToOne sở hữu foreign key. @JoinColumn là tùy chọn trên @ManyToOne (TypeORM suy luận ra), nhưng đặt tên rõ ràng tránh bất ngờ khi tên cột quan trọng cho raw query hoặc diff migration.
Với relation many-to-many, TypeORM tạo junction table tự động:
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 phải xuất hiện chính xác ở một phía của relation. Nó xác định entity nào sở hữu junction table và kiểm soát tên của nó.
Sẵn sàng chinh phục phỏng vấn Node.js / NestJS?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Repository Pattern và Custom Repository
Hệ thống module và DI của NestJS tích hợp repository TypeORM thông qua TypeOrmModule.forFeature(). Inject Repository<T> mặc định đáp ứng các thao tác CRUD tiêu chuẩn. Khi query trở nên phức tạp, custom repository đóng gói logic đó.
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 {}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() xử lý các query đơn giản. QueryBuilder xử lý join, subquery, aggregation và mọi thứ cần kiểm soát SQL chi tiết. Kết hợp cả hai trong cùng service là hoàn toàn hợp lệ: dùng find() cho lookup đơn giản và QueryBuilder khi query không thể biểu diễn qua options API.
Quản lý Transaction cho Toàn vẹn Dữ liệu
Các thao tác sửa đổi nhiều bảng cần transaction. TypeORM cung cấp hai cách tiếp cận: method DataSource.transaction() và API QueryRunner.
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() dựa trên callback tự động commit khi thành công và rollback khi có error được throw. Mọi thao tác database trong callback phải sử dụng manager được cung cấp thay vì repository được inject, nếu không các thao tác đó chạy ngoài transaction.
Sử dụng DataSource.transaction() cho các thao tác đa bước đơn giản. Chuyển sang QueryRunner khi cần kiểm soát thủ công các điểm commit/rollback hoặc savepoint, như partial rollback trong batch processing.
Câu hỏi Phỏng vấn NestJS TypeORM Thường gặp
Các câu hỏi này thường xuất hiện trong phỏng vấn backend developer. Mỗi câu trả lời nhắm đến độ sâu kỳ vọng trong thảo luận kỹ thuật cấp senior.
Tại sao synchronize: true không bao giờ được sử dụng trong production?
Auto-sync áp dụng thay đổi schema phá hủy mà không xác nhận. Xóa cột, thay đổi kiểu, hoặc xóa index xảy ra ngay lập tức với dữ liệu đang chạy. Migration cung cấp version control, code review và khả năng rollback. Cách sử dụng synchronize an toàn duy nhất là phát triển local với dữ liệu có thể bỏ.
Sự khác biệt giữa relation eager và lazy là gì?
Relation eager được load tự động với mọi query trên entity cha. Relation lazy trả về Promise và thực thi query riêng chỉ khi được truy cập. Eager loading gây overhead kiểu N+1 khi dữ liệu liên quan không cần thiết. Lazy loading trì hoãn chi phí nhưng có thể kích hoạt query bất ngờ sâu trong call stack. Tùy chọn relations rõ ràng trong find() hoặc leftJoinAndSelect trong QueryBuilder cung cấp toàn quyền kiểm soát những gì được load theo query.
cascade hoạt động thế nào trong relation TypeORM?
Đặt cascade: true trên relation nghĩa là lưu entity cha cũng lưu các entity con chưa được lưu. cascade: ['insert', 'update'] giới hạn hành vi cho các thao tác cụ thể. Cascade hoạt động ở cấp ORM, không phải cấp database. Với cascading delete cấp database, sử dụng onDelete: 'CASCADE' trên decorator @ManyToOne, tạo constraint ON DELETE CASCADE trong migration.
Khi nào nên sử dụng custom repository thay vì Repository<T> mặc định?
Custom repository đóng gói logic query phức tạp không thuộc về service. Dấu hiệu để tách custom repository: cùng một chain QueryBuilder xuất hiện trong nhiều service, query liên quan đến nhiều join hoặc subquery, hoặc query yêu cầu raw SQL. Các lời gọi find() đơn giản không đáng để tạo custom repository.
Với các câu hỏi kiến trúc NestJS khác, xem hướng dẫn đầy đủ về guards và interceptors NestJS.
Sẵn sàng chinh phục phỏng vấn Node.js / NestJS?
Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.
Nguồn tham khảo
- TypeORM 1.0.0 Release - Yêu cầu ECMAScript 2023, loại bỏ Connection, mặc định validation nghiêm ngặt hơn
- TypeORM 1.1.2 Release - Sửa lỗi rò rỉ kết nối trong DataSource.initialize(), sửa junction row many-to-many
- NestJS 12 Release - Chuyển đổi ESM, yêu cầu Node.js 20.19+/22.12+
- @nestjs/typeorm 12.0.0 - Native ES modules, tương thích CommonJS qua Node require(esm)
Điểm chính cho Ứng dụng NestJS TypeORM Production
- Sử dụng file cấu hình
DataSourceriêng cho CLI TypeORM, độc lập với cấu hình module NestJS - Không bao giờ bật
synchronizetrong production; dựa vào migration được tạo và xem xét cho mọi thay đổi schema - Đặt
@ManyToOnetrên entity sở hữu foreign key, và sử dụng đặt tên@JoinColumnrõ ràng để rõ ràng trong raw query và output migration - Chọn giữa options API
find()cho query đơn giản vàQueryBuildercho join phức tạp, aggregation và logic có điều kiện - Bọc mutation đa bảng trong transaction sử dụng
DataSource.transaction(), và đảm bảo mọi thao tác trong callback sử dụngmanagertransactional - Với TypeORM 1.1.x ổn định hóa API surface và NestJS 12 hoàn thành chuyển đổi ESM, stack này vẫn là lựa chọn mạnh cho backend doanh nghiệp cần truy cập database có cấu trúc, type-safe
Bắt đầu luyện tập!
Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.
Bạn có tìm ra lỗi trong Node.js / NestJS không?
Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Viết bởi
Anthony Fillion-MailletNgười sáng lập SharpSkill
Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.
Cập nhật ngày 21 tháng 10, 2026
Thẻ
Chia sẻ
Bài viết liên quan

NestJS + Prisma: stack backend hiện đại cho Node.js
Hướng dẫn đầy đủ để xây dựng API backend hiện đại với NestJS và Prisma. Cấu hình, model, service, transaction và các best practice được giải thích chi tiết.

NestJS: Xây dựng REST API hoàn chỉnh từ đầu
Hướng dẫn đầy đủ xây dựng REST API chuyên nghiệp với NestJS. Controller, Service, Module, xác thực dữ liệu với class-validator và xử lý lỗi được giải thích chi tiết.

NestJS và MongoDB trong 2026: Mongoose, Aggregation và Câu hỏi Phỏng vấn
Thành thạo NestJS với MongoDB và Mongoose 9. Học thiết kế schema, aggregation pipeline, và chuẩn bị phỏng vấn kỹ thuật với các ví dụ thực tế.