When backend code starts getting messy
Many backend codebases become confusing here:
DTOis passed directly into theserviceEntityis used directly in business logicservicereturnsEntityback 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.
Short answer first
If you want the short version:
DTObelongs in thecontrollerfor input validation and request contractsModelbelongs in theservicefor business logicEntityshould stay attached toTypeORMor the database layer
In simpler terms:
DTOis what comes into the systemModelis what the system thinks withEntityis 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, orclass-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-validatorTypeORM- 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:
ControllerreceivesDTOServiceworks withModelRepositoryor persistence layer works withEntity
Typical flow:
- Controller receives
CreateAccountDto - Controller maps it to
AccountModel - Service applies business logic to
AccountModel - Repository maps
AccountModeltoAccountEntity - TypeORM stores
AccountEntity - Repository maps
AccountEntityback toAccountModel
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:
Entitydoes not leak directly into the service layer unless you explicitly want thatservicekeeps 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
Entitydirectly as the API response model - passing
DTOdirectly into the service layer - letting the service depend on
class-validatorobjects - putting business methods into
Entityand 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:
DTOis for input and validation in thecontrollerModelis for business logic in theserviceEntityis 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.