DTO
MOD
ENT
Backend Layers
controller • service • db

Cách phân biệt DTO, Model và Entity trong backend

Cách phân biệt DTO, Model và Entity trong backend để tránh dùng lẫn giữa controller, service và database, đặc biệt khi dùng TypeORM và validate đầu vào.

12 phút đọc18/06/2026

Khi nào code bắt đầu rối giữa DTO, Model và Entity

Rất nhiều codebase backend bị rối ở chỗ này:

  • DTO bị truyền thẳng xuống service
  • Entity bị dùng luôn trong business logic
  • service trả Entity ngược ra controller
  • frontend, controller, service và database cùng kéo nhau bằng một kiểu object

Ban đầu cách này có vẻ nhanh. Nhưng càng về sau càng lộ vấn đề:

  • code khó reuse
  • khó test
  • phụ thuộc chéo giữa framework và business logic
  • đổi database hoặc đổi ORM là vỡ dây chuyền

Nếu bạn đang dùng NestJS hoặc backend chia tầng rõ hơn, thì việc tách DTO, ModelEntity ra đúng vai trò là rất quan trọng.

Sơ đồ luồng DTO, Model và Entity trong backend: Controller nhận DTO để validate, Service xử lý bằng Model, Repository/TypeORM làm việc với Entity rồi map lại về Model

Kết luận nhanh trước khi đi sâu

Nếu cần chốt ngắn gọn:

  • DTO dùng ở controller để nhận input, validate và define contract request
  • Model dùng ở service để xử lý business logic
  • Entity chỉ nên gắn với TypeORM hoặc tầng database

Nói dễ hiểu hơn:

  • DTO là dữ liệu đi vào hệ thống
  • Model là dữ liệu dùng để hệ thống suy nghĩ
  • Entity là dữ liệu dùng để lưu xuống database

DTO là gì và nên dùng ở đâu

DTO là viết tắt của Data Transfer Object.

Trong backend hiện đại, đặc biệt với NestJS, DTO thường được dùng để:

  • nhận request body
  • nhận query param
  • nhận route param
  • validate đầu vào
  • mô tả rõ contract mà controller chấp nhận

Ví dụ:

export class CreateAccountDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  @IsDateString()
  birthday: string;
}

Ở đây ý nghĩa của DTO là:

  • client gửi gì vào
  • backend chấp nhận format nào
  • field nào bắt buộc
  • field nào sai thì reject ngay từ controller

Vì sao DTO chỉ nên dừng ở controller

Đây là chỗ nhiều team dùng sai nhất.

DTO gắn rất chặt với:

  • request format
  • validation decorator
  • framework layer như controller, pipe, class-validator

Nếu nhét DTO xuống service, bạn sẽ gặp các vấn đề:

  • service bị phụ thuộc vào request shape của controller
  • khó reuse cùng logic từ job, cron, queue, event consumer
  • logic business bị lệ thuộc vào class validation hoặc HTTP input
  • object đi vào service không còn thuần business nữa

Một vấn đề thực tế khác là:

  • DTO thường không được thiết kế như một object business để new lên và truyền khắp nơi
  • nó sinh ra để nhận dữ liệu đầu vào và validate, không phải để sống lâu trong các tầng bên dưới

Vì vậy:

  • controller nhận DTO
  • validate xong
  • map sang Model
  • rồi mới đưa vào service

Model là gì và nên dùng ở đâu

Trong bài này, Model nên hiểu là object đại diện cho dữ liệu business mà tầng service muốn làm việc cùng.

Model không nên phụ thuộc vào:

  • class-validator
  • TypeORM
  • decorator của database
  • HTTP request

Ví dụ:

export class AccountModel {
  constructor(
    public readonly id: string | null,
    public readonly name: string,
    public readonly email: string,
    public readonly birthday: Date | null,
  ) {}
}

Model hợp với service vì:

  • nó thuần business hơn
  • dễ new
  • dễ test
  • dễ reuse từ nhiều entry point khác nhau

Vì sao service nên làm việc với Model thay vì DTO

Giả sử controller nhận:

export class GetAccountDto {
  @IsUUID()
  accountId: string;
}

Nếu bạn truyền thẳng GetAccountDto xuống service, service đang bị kéo theo request contract.

Cách tốt hơn là:

export class AccountLookupModel {
  constructor(public readonly accountId: string) {}
}

Controller:

@Get(":accountId")
getAccount(@Param() dto: GetAccountDto) {
  const model = new AccountLookupModel(dto.accountId);
  return this.accountService.getAccount(model);
}

Service:

getAccount(model: AccountLookupModel) {
  // business logic
}

Lợi ích là:

  • sau này queue consumer cũng gọi lại được getAccount
  • cron job cũng gọi lại được
  • test unit cũng tạo new AccountLookupModel(...) dễ dàng
  • service không cần biết request ban đầu là HTTP, queue hay CLI

Đây chính là ý nghĩa lớn nhất của Model ở tầng service:

  • giúp business logic dễ reuse

Entity là gì và nên dùng ở đâu

Entity thường là object gắn với ORM hoặc database.

Nếu dùng TypeORM, Entity thường có:

  • @Entity()
  • @Column()
  • @PrimaryGeneratedColumn()
  • relation
  • mapping database

Ví dụ:

@Entity("accounts")
export class AccountEntity {
  @PrimaryColumn("uuid")
  id: string;

  @Column()
  name: string;

  @Column()
  email: string;

  @Column({ type: "date", nullable: true })
  birthday: string | null;
}

Vai trò đúng của Entity là:

  • map với bảng database
  • để ORM load/save/query
  • đại diện cho persistence layer

Nó không nên trở thành object business đi khắp codebase.

Vì sao không nên dùng Entity thẳng ở service

Nếu service xử lý trực tiếp bằng Entity, bạn sẽ dính:

  • business logic phụ thuộc ORM
  • relation lazy/eager ảnh hưởng logic không mong muốn
  • test unit khó hơn
  • đổi ORM hoặc đổi cách lưu dữ liệu sẽ kéo theo service

Tệ hơn nữa là Entity rất dễ bị dùng sai như một object “đa năng”:

  • vừa là request contract
  • vừa là business object
  • vừa là database object

Khi một class ôm cả 3 vai trò này, code sẽ ngày càng khó kiểm soát.

Cách đúng: Entity chỉ ở repository hoặc database layer

Một cách tổ chức dễ giữ code sạch là:

  • Controller nhận DTO
  • Service làm việc với Model
  • Repository hoặc database layer làm việc với Entity

Ví dụ flow:

  1. Controller nhận CreateAccountDto
  2. Controller map sang AccountModel
  3. Service xử lý business trên AccountModel
  4. Repository map AccountModel sang AccountEntity
  5. TypeORM lưu AccountEntity
  6. Repository map lại AccountEntity về AccountModel

Nên map Entity sang Model như thế nào

Đây là chỗ pattern kiểu adapter hoặc mapper rất hợp.

Ví dụ:

@Entity("accounts")
export class AccountEntity {
  @PrimaryColumn("uuid")
  id: string;

  @Column()
  name: string;

  @Column()
  email: string;

  @Column({ type: "date", nullable: true })
  birthday: string | null;

  toModel(): AccountModel {
    return new AccountModel(
      this.id,
      this.name,
      this.email,
      this.birthday ? new Date(this.birthday) : null,
    );
  }
}

Hoặc dùng mapper riêng:

export class AccountMapper {
  static toModel(entity: AccountEntity): AccountModel {
    return new AccountModel(
      entity.id,
      entity.name,
      entity.email,
      entity.birthday ? new Date(entity.birthday) : null,
    );
  }
}

Cả 2 cách đều ổn. Điểm quan trọng là:

  • Entity không bị lộ thẳng lên service nếu bạn không muốn
  • service luôn làm việc với một kiểu dữ liệu business ổn định hơn

DTO, Model và Entity khác nhau như thế nào

Loại Mục đích Nên ở tầng nào Không nên dùng cho gì
DTO nhận input, validate, định nghĩa request contract Controller business logic lâu dài, repository
Model biểu diễn dữ liệu business để service xử lý Service validate HTTP, mapping database trực tiếp
Entity map ORM và database Repository / database layer request contract, business object chính

Một flow ví dụ hoàn chỉnh

Ví dụ tạo account:

DTO ở controller

export class CreateAccountDto {
  @IsString()
  name: string;

  @IsEmail()
  email: string;

  @IsDateString()
  birthday: string;
}

Model ở service

export class AccountModel {
  constructor(
    public readonly id: string | null,
    public readonly name: string,
    public readonly email: string,
    public readonly birthday: Date | null,
  ) {}
}

Controller map DTO sang Model

@Post()
createAccount(@Body() dto: CreateAccountDto) {
  const model = new AccountModel(
    null,
    dto.name,
    dto.email,
    dto.birthday ? new Date(dto.birthday) : null,
  );

  return this.accountService.createAccount(model);
}

Service xử lý business bằng Model

createAccount(model: AccountModel) {
  // check duplicate email
  // apply business rules
  return this.accountRepository.save(model);
}

Repository map Model sang Entity

async save(model: AccountModel): Promise<AccountModel> {
  const entity = new AccountEntity();
  entity.id = model.id ?? randomUUID();
  entity.name = model.name;
  entity.email = model.email;
  entity.birthday = model.birthday
    ? model.birthday.toISOString().slice(0, 10)
    : null;

  const saved = await this.repo.save(entity);
  return saved.toModel();
}

Flow này nhìn có vẻ dài hơn một chút, nhưng đổi lại:

  • code tách tầng rõ
  • service dễ reuse
  • test dễ hơn
  • đổi ORM hoặc đổi transport layer ít ảnh hưởng hơn

Khi nào có thể đơn giản hơn

Không phải project nào cũng cần tổ chức quá nặng.

Nếu app rất nhỏ, đôi khi bạn có thể gom nhẹ hơn. Nhưng vẫn nên giữ nguyên tinh thần:

  • DTO không đi sâu xuống service
  • Entity không nổi lên business layer

Ngay cả khi chưa tách đủ class riêng, việc giữ đúng ranh giới tư duy vẫn rất đáng làm.

Những lỗi phổ biến khi dùng lẫn DTO, Model và Entity

  • dùng Entity làm luôn response cho API
  • truyền DTO thẳng xuống service
  • để service nhận object phụ thuộc class-validator
  • thêm method business vào Entity rồi để cả hệ thống phụ thuộc vào ORM class đó
  • để controller gọi repository trực tiếp vì “service chỉ pass through”

Các lỗi này thường làm codebase nhanh phình nợ kỹ thuật hơn mọi người tưởng.

Kết luận

Nếu muốn code backend sạch và dễ reuse hơn, bạn nên nhớ:

  • DTO dùng để nhận và validate input ở controller
  • Model dùng để xử lý business ở service
  • Entity dùng cho ORM và database

Đừng để một object ôm cả 3 vai trò cùng lúc.

Khi tách đúng:

  • controller rõ contract
  • service rõ business
  • repository rõ persistence

Đó mới là ý nghĩa đúng của việc phân biệt DTO, ModelEntity trong source code backend.