DTO
MOD
ENT
Backend Layers
controller • service • db

How to separate DTO, Model, and Entity in backend code

How to separate DTO, Model, and Entity in backend code so controller, service, and database layers do not get mixed together, especially with TypeORM.

12 min read18/06/2026

When backend code starts getting messy

Many backend codebases become confusing here:

  • DTO is passed directly into the service
  • Entity is used directly in business logic
  • service returns Entity back to the controller
  • frontend, controller, service, and database all get tied to the same object shape

At first this feels fast. Later it creates problems:

  • code is harder to reuse
  • testing gets harder
  • business logic becomes coupled to framework details
  • changing ORM or persistence decisions breaks too much code

If you use NestJS or a layered backend structure, separating DTO, Model, and Entity correctly matters a lot.

DTO, Model, and Entity flow in a backend: controller receives DTO for validation, service works with Model, repository and TypeORM use Entity and map back to Model

Short answer first

If you want the short version:

  • DTO belongs in the controller for input validation and request contracts
  • Model belongs in the service for business logic
  • Entity should stay attached to TypeORM or the database layer

In simpler terms:

  • DTO is what comes into the system
  • Model is what the system thinks with
  • Entity is what the system stores

What a DTO is and where it should be used

DTO stands for Data Transfer Object.

In modern backends, especially with NestJS, DTOs are usually used to:

  • receive request bodies
  • receive query params
  • receive route params
  • validate input
  • define the contract the controller accepts

Example:

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

  @IsEmail()
  email: string;

  @IsDateString()
  birthday: string;
}

The point of the DTO here is:

  • what the client sends
  • what format the backend accepts
  • which fields are required
  • which invalid inputs should be rejected immediately

Why DTOs should usually stop at the controller

This is one of the most common mistakes.

DTOs are tightly coupled to:

  • request format
  • validation decorators
  • framework concerns like controller, pipe, or class-validator

If you push DTOs down into the service layer, you usually get:

  • services coupled to controller request shapes
  • less reuse from jobs, queues, cron tasks, or event consumers
  • business logic tied to HTTP input or validation classes
  • objects in the service layer that are no longer truly business-oriented

Another practical problem is:

  • DTOs are typically not designed as business objects you instantiate and pass around everywhere
  • they exist to validate incoming data, not to live deep in the system

So the cleaner path is:

  • controller receives the DTO
  • validation happens there
  • map to a Model
  • only then call the service

What a Model is and where it should be used

In this article, Model means a business-oriented object that the service layer uses to reason about data.

A Model should not depend on:

  • class-validator
  • TypeORM
  • database decorators
  • HTTP request shapes

Example:

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

Models work well in services because they are:

  • more business-focused
  • easy to instantiate
  • easy to test
  • easy to reuse from multiple entry points

Why services should use Model instead of DTO

Assume the controller receives:

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

If you pass GetAccountDto directly into the service, the service is now tied to the request contract.

A cleaner option is:

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
}

That gives you:

  • queue consumers that can reuse the same service method
  • cron jobs that can reuse it too
  • easier unit tests
  • no service dependency on whether the request came from HTTP, CLI, or a queue

That is the real value of using a Model in the service layer:

  • better business-logic reuse

What an Entity is and where it should be used

An Entity is usually tied to the ORM or persistence layer.

With TypeORM, an entity commonly contains:

  • @Entity()
  • @Column()
  • @PrimaryGeneratedColumn()
  • relations
  • database mapping details

Example:

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

  @Column()
  name: string;

  @Column()
  email: string;

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

The correct role of an entity is:

  • mapping to database tables
  • letting the ORM load, save, and query data
  • representing the persistence layer

It should not become the main business object used everywhere.

Why services should not work directly with Entity

If services work directly on entities, you often get:

  • business logic coupled to the ORM
  • lazy or eager relations affecting logic in surprising ways
  • harder unit tests
  • more pain when changing persistence decisions later

The worst version is when an entity becomes a “universal object”:

  • request contract
  • business object
  • database object

Once one class tries to serve all three roles, the codebase gets harder to control.

The cleaner approach: keep Entity in the repository layer

A clean structure is usually:

  • Controller receives DTO
  • Service works with Model
  • Repository or persistence layer works with Entity

Typical flow:

  1. Controller receives CreateAccountDto
  2. Controller maps it to AccountModel
  3. Service applies business logic to AccountModel
  4. Repository maps AccountModel to AccountEntity
  5. TypeORM stores AccountEntity
  6. Repository maps AccountEntity back to AccountModel

How to map Entity to Model

This is where an adapter or mapper pattern works well.

Example:

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

Or with a dedicated mapper:

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

Both are reasonable. The important part is:

  • Entity does not leak directly into the service layer unless you explicitly want that
  • service keeps working with a more stable business object

DTO vs Model vs Entity

Type Purpose Best layer Should not be used for
DTO input validation and request contracts Controller long-lived business logic, repository logic
Model business-oriented data for services Service HTTP validation, direct database mapping
Entity ORM and database mapping Repository / persistence layer request contracts, main business object

A complete example flow

For account creation:

DTO in controller

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

  @IsEmail()
  email: string;

  @IsDateString()
  birthday: string;
}

Model in service

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

Controller maps DTO to 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 applies business logic

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

Repository maps Model to 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();
}

This is a little more structure, but it gives you:

  • cleaner layering
  • easier reuse
  • easier testing
  • less coupling to ORM or transport choices

When can you simplify

Not every project needs a heavy architecture.

If the app is very small, you may simplify. But even then, the separation principle still matters:

  • DTO should not sink deep into the service layer
  • Entity should not rise into the business layer

Even without a lot of classes, keeping the role boundaries clear is still worth it.

Common mistakes when DTO, Model, and Entity get mixed

  • using Entity directly as the API response model
  • passing DTO directly into the service layer
  • letting the service depend on class-validator objects
  • putting business methods into Entity and making the whole system depend on the ORM class
  • letting the controller call the repository directly because “the service only passes through”

These mistakes usually create more technical debt than people expect.

Conclusion

If you want cleaner and more reusable backend code, remember this:

  • DTO is for input and validation in the controller
  • Model is for business logic in the service
  • Entity is for ORM and database work

Do not let one object play all three roles.

When the separation is correct:

  • controller owns the contract
  • service owns the business logic
  • repository owns persistence

That is the real meaning of separating DTO, Model, and Entity in backend source code.