Parsing External Data with DTOs (Data Transfer Objects) – Practical Javascript and Ruby Guide

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

Scroll to Top