DTO in JavaScript

Information systems are designed to process data, and DTO (Data Transfer Object) is an important concept in modern development. In the “classical” sense, DTOs are simple objects (without logic) that describe data structures that are transferred “over the network” between remote processes. If data is transferred between application layers within the same process, then such DTOs are called local DTOs.

The key objectives of a DTO are:

Here, I will outline the principles I follow when building DTOs in my JavaScript applications.


Simple DTO

In a simple case, the DTO is a flat structure where each attribute is a primitive data type, such as a string or integer:

class Simple {
 /** @type {boolean} */
 aBool;

 /** @type {number} */
 aNumber;

 /** @type {string} */
 aString;
}

It’s about data structuring (the first objective). The second objective is data transformation. We need to be able to parse some input data and convert it into our structure while casting data types:

class Simple {
 constructor(data) {
     this.aBool = Boolean(data?.aBool);
     this.aNumber = Number.parseFloat(data?.aNumber);
     this.aString = String(data?.aString);
 }
}

Complex DTO

A complex DTO consists of other DTOs (complex and simple) and primitives:

class Complex {
 /** @type {Simple} */
 aDto;

 /** @type {string} */
 aString;

 constructor(data) {
     this.aDto = new Simple(data?.aDto);
     this.aString = String(data?.aString);
 }
}

JSON-to-DTO’ transformation in this case looks like this:

const dto = new Complex({
 aDto: {
     aBool: true,
     aNumber: 16,
     aString: 'simple',
 },
 aString: 'complex',
});

Waterfall Type Casting

For waterfall casting of types in complex objects, we need to connect the code sources with import-export:

// simple.mjs
export default class Simple {}

// complex.mjs
import Simple from './simple.mjs';

export default class Complex {}

This approach helps manage the complexity of nested DTOs. Each DTO handles its own data parsing and type casting, allowing us to divide responsibilities among independent modules. By connecting the sources, we ensure that any changes in one component (e.g., Simple) don’t disrupt the overall structure of the application.


Cutting vs. Casting

Let’s say we have JSON data:

{
"name": "Alex Gusev",
"age": "32",
"weight": "64"
}

And a DTO for this data:

class Person {
 /** @type {string} */
 name;

 /** @type {number} */
 age;
}

In general, we may receive data that differs from what we are prepared to process. In such cases, there are two ways to convert the data to a DTO:

Strategy 1: Cutting Off Extra Data

class Person {
 constructor(data) {
     this.name = String(data?.name);
     this.age = Number.parseInt(data?.age);
 }
}

Strategy 2: Casting Types for Known Attributes

class Person {
 constructor(data) {
     Object.assign(this, data);
     this.name = String(data?.name);
     this.age = Number.parseInt(data?.age);
 }
}

Here is the result for both cases:

Screenshot

If our code is the final DTO handler, it is better to eliminate any unnecessary data. If it is an intermediate handler, it is better to handle the casting of data types that we directly interact with and leave any unfamiliar data unchanged.


Resume