How DTOs (Data Transfer Objects) Work — Explained
The Problem: What Should an API Response Actually Contain?
Following the DB Repository Pattern, by the time data reaches your service file, it has already gone through two object types:
- The ORM/3rd-party lib object (e.g.
objectionjs.Model) — returned by the DB driver/ORM. - The data model / domain model object (e.g.
UserDomainModel) — returned by the repository file, stripped of any ORM-specific type.
Neither of these should be sent directly to the client as an API response.
Why You Cannot Send the ORM Object to the Client
The ORM/3rd-party lib object can carry internal fields added by the library itself — properties and methods attached by objection.js, pg, etc. that have nothing to do with your business data. You don't control what the library decides to attach to this object, and it can change between library versions. Sending this object as-is means leaking implementation details — and potentially sensitive internal state — straight to the client.
Why You Cannot Send the Domain Model Object to the Client Either
The domain model object is safe from ORM leakage, but it isn't safe for the client. It typically holds every field needed by the service layer to do its job — including fields like password (hashed), internal flags, or audit columns — because the service file may need these for its own logic (e.g. comparing a hashed password during login, checking an is_blocked flag).
These fields are required internally but must never be exposed in an API response.
The Solution: The DTO (Data Transfer Object)
A DTO is a new object type whose only job is to represent exactly what is allowed to travel across the network — either received from the client (request DTO) or sent back to the client (response DTO).
A DTO contains only the fields the client is allowed to see or send — nothing more.
"Why Not Just Filter Out the Fields We Don't Want?"
You might think: why introduce a whole new object type? Can't we just loop over the domain model object and strip the fields we don't want before sending the response?
That's exactly what a DTO is. Filtering out unneeded fields and creating a new object/array of objects with only the needed fields is the DTO — whether you do it via a manual filter loop, a mapping function, or a UserResponseDto class. The DTO is the concept of "the object that is allowed to be transferred over the network"; the filter loop, the plain object literal, and the class are just different ways of implementing that concept.
Implementing a DTO with a Class
Using a class makes the allowed shape explicit and reusable:
class UserResponseDto {
id;
name;
email;
constructor(domainModel) {
this.id = domainModel.id;
this.name = domainModel.name;
this.email = domainModel.email;
// password, is_blocked, etc. are intentionally NOT copied here
}
}
users-service.js
...
const user = await this.userRepository.getUserById(id); // UserDomainModel — has password, is_blocked, etc.
...
return new UserResponseDto(user); // only id, name, email leave the service
...
The class constructor only copies over the fields explicitly listed — any field on the domain model that isn't listed simply never makes it into the DTO, regardless of what the domain model (or, further down, the ORM) attaches to its object.
Request DTOs Work the Same Way, in Reverse
The same concept applies to incoming data. A request DTO defines exactly which fields are accepted from the client — anything else in the incoming payload is ignored rather than being blindly passed down into the service/repository layers.
class CreateUserRequestDto {
name;
email;
password;
constructor(body) {
this.name = body.name;
this.email = body.email;
this.password = body.password;
// any other field the client sent (e.g. role, is_admin) is dropped here
}
}
This also protects against a client trying to set fields it shouldn't be able to set directly (e.g. sending "is_admin": true in a signup request).
Summary

- Data passes through three different object shapes: the ORM object (3rd-party type, untrusted shape), the domain model object (full internal shape, used by the service layer), and the DTO (only what's allowed to cross the network).
- The ORM object can't be sent to the client because the library can attach its own internal fields that you don't control.
- The domain model object can't be sent to the client because it holds fields the service layer needs internally (e.g.
password) that must never be exposed. - A DTO is simply the object containing only the fields allowed to travel over the network — for a response, or for a request.
- "Just filtering out unneeded fields" is not an alternative to a DTO — it's an implementation of one. A filter loop, a mapping function, and a DTO class all produce the same outcome; the class just makes the allowed shape explicit and reusable.
- Request DTOs apply the same idea in reverse — defining exactly which incoming fields are accepted, so unexpected or unauthorized fields in a request body are dropped before reaching the service/repository layers.