Why DTOs
DTOs are particularly useful when:
- Working with external APIs that return complex or deeply nested JSON responses.
- Performing transformations on incoming data before it’s used by our applications.
- Standardizing the way your application interacts with external data sources.
When to use them
When fetching data from an API, it may be good idea translate the API specific response to our Domain Entities. This way, if the external API changes, we only need to update one place, ensuring that the rest of our application remains unaffected.
Handling such data directly within our domain code/models/controllers/etc, can lead to tightly coupled/less maintainable code.
In these situations, using a DTO (Data Transfer Object) is a recommended practice. A DTO acts as a middleman layer between the unstructured data and our domain objects. This separation ensures that our domain models remain focused on business logic while the DTO takes responsibility for data parsing.
What is a DTO (Data Transfer Object)?
A DTO:
- Defines methods to fetch the "JSON attributes values"
- Handles serialization and deserialization .
- Should NEVER have methods with business logic
Here are two examples using Ruby and Javascript.
Ruby Example
Let’s say we receive the following JSON response from an external API:
{
"title": "New Sushi Project",
"budget": 50000.0,
"members": [
{ "full_name": "Viny" },
{ "full_name": "Jiro" }
]
}
And our domain classes uses "name" instead of "title" or "full_name":
class Project
attr_accessor :name, :budget, :members
def initialize(name:, budget:, members:)
@name = name
@budget = budget
@members = members
end
end
class Project::Member
attr_accessor :name
def initialize(name:)
@name = name
end
end
We can implement a DTO for a Project this that has many Project::Members:
# app/dtos/member_dto.rb
class ProjectMemberDTO
attr_reader :name
def initialize(data)
@name = data[:full_name]
end
def to_h
{ name: @name }
end
def to_json(*_args)
to_h.to_json
end
def to_model
Project::Member.new(to_h)
end
end
# app/dtos/project_dto.rb
class ProjectDTO
attr_reader :name, :budget, :members
def initialize(json)
data = JSON.parse(json, symbolize_names: true)
@name = data[:title]
@budget = data[:budget]
@members = data[:members]&.map { |member_data| ProjectMemberDTO.new(member_data) }
end
def to_h
{
name: @name,
budget: @budget,
members: @members.map(&:to_h)
}
end
def to_json(*_args)
to_h.to_json
end
def to_model
Project.new(to_h.merge(members: @members.map(&:to_model)))
end
end
Usage
We can use our DTO to transform and work with this data:
json_response = '{"title":"New Sushi Project","budget":50000.0,"members":[{"full_name":"Viny"},{"full_name":"Jiro"}]}'
project_dto = ProjectDTO.new(json_response)
puts project_dto.name # => "New Sushi Project"
puts project_dto.budget # => 50000.0
puts project_dto.members # => MembersDTO Object ("Viny", "Jiro")
# Convert back to JSON if needed
puts project_dto.to_json
# => {"name":"New Sushi Project","budget":50000.0,"members":[{"name":"Viny"},{"name":"Jiro"}]}
puts project_dto.to_model.inspect
# #<Project:0x0000000100ba9e28 @name="New Sushi Project", @budget=50000.0, @members=[#<Project::Member:0x0000000100ba9ef0 @name="Viny">, #<Project::Member:0x0000000100ba9ea0 @name="Jiro">]>
Using Ruby Structs/Data for DTOs
You can use them to create your DTOs. But don’t inherit from them.
It has some bad side effects like:
- creates an anonymous class in the ancestor chain making debbuging difficult
- cause problems with code reload
class ProjectDTO < Struct.new(:name)
end
ProjectDTO2 = Struct.new(:name) do
end
p ProjectDTO.ancestors # => [ProjectDTO, #<Class:0x00000001050642e8>, Struct, Enumerable, Object, Kernel, BasicObject]
p ProjectDTO2.ancestors # => [ProjectDTO2, Struct, Enumerable, Object, Kernel, BasicObject]
For more info, check this post:
https://thepugautomatic.com/2013/08/struct-inheritance-is-overused/
Javascript Example
Let’s say we receive the following JSON response from an external API:
{
"title": "New Sushi Project",
"budget": 50000.0,
"members": [
{ "full_name": "Viny" },
{ "full_name": "Jiro" }
]
}
And our domain classes uses "name" instead of "title" or "full_name":
class Project {
constructor({ name, budget, members }) {
this.name = name;
this.budget = budget;
this.members = members;
}
}
class ProjectMember {
constructor({ name }) {
this.name = name;
}
}
Implementation of a DTO for a Project model that has many ProjectMembers:
// member_dto.js
class ProjectMemberDTO {
constructor(data) {
this.name = data.full_name; // Map the `full_name` field
}
toObject() {
return { name: this.name };
}
toJSON() {
return JSON.stringify(this.toObject());
}
toModel() {
return new ProjectMember(this.toObject());
}
}
// project_dto.js
class ProjectDTO {
constructor(json) {
const data = JSON.parse(json);
this.name = data.title; // Map the `title` field
this.budget = data.budget;
this.members = (data.members || []).map(memberData => new ProjectMemberDTO(memberData));
}
toObject() {
return {
name: this.name,
budget: this.budget,
members: this.members.map(member => member.toObject()),
};
}
toJSON() {
return JSON.stringify(this.toObject());
}
toModel() {
return new Project({
...this.toObject(),
members: this.members.map(memberDTO => memberDTO.toModel()),
});
}
}
We can use our DTO to transform and work with this data:
const jsonResponse = '{"title":"New Sushi Project","budget":50000.0,"members":[{"full_name":"Viny"},{"full_name":"Jiro"}]}'
const projectDTO = new ProjectDTO(jsonResponse);
console.log(projectDTO.name); // "New Sushi Project"
console.log(projectDTO.budget); // 50000.0
console.log(projectDTO.members.map(member => member.name)); // ["Viny", "Jiro"]
console.log(projectDTO.toModel());
/*
New Sushi Project
50000
[ 'Viny', 'Jiro' ]
Project {
name: 'New Sushi Project',
budget: 50000,
members: [ ProjectMember { name: 'Viny' }, ProjectMember { name: 'Jiro' } ]
}
*/
Conclusion
When DTOs May Not Be Necessary:
- Small apps where direct JSON manipulation is sufficient.
- When data structures are flat/don’t require much transformations.
- If there is no need to decouple external data format from your application logic.
While JavaScript/Ruby are more dynamic/flexible when compared to statically languages like Java, DTOs can still provide several benefits in specific scenarios. Examples:
- Handling complex or nested API responses in the frontend
- When interacting with APIs or external services that return deeply nested JSON data.
- Decouple our Domain Objects from the API response structure.
- Mapping one API response to multiple Domain Objects
That’s all.
Thanks!
Bonus: random thoughts
DTOs is an a pattern from Java. It was used to pass data across our app layers without having the need to pass – for example – our domain objects with a lot of fields. It helps managing shared dependencies between layers (instead of a layer depending on another, both can have the DTO as dependency) and serialization/deserialization.
In languages like JS/Ruby where we "can just pass" JSON/Hashes that to parts of our system (given that no abstractions/layers separation are disrespected),
I don’t know if DTO is the best name but it’s the name that everyone still uses for similar cases from this post.
Subscribe to my newsletter!
I share content about Software Development & Architecture, Entrepreneurship and Lifelong Learning




