0

I've recently started learning Nestjs and backend development in general. I got familiarized with the concept of DTO. Each and every tutorial/article on the Internet focuses on their usage for accepting and sending API requests. And what about the data transfer between the app and the database? Just like the web, databases are part of the infrastructure layer. That means that we need to create an abstraction between the database and the rest of the app, as well as decouple the persistence from the domain. After all, my persistence level differs a lot from the domain level. So I thought DTOs with some mapping might be helpful. But I'm wondering, how do you 'translate' data from and to persistence? Do you have any special DTO types? Or do you simply preform mapping manually in the repository? Does the 'translation' path look like 'persistence' --> 'domain' --> 'view' (for read queries) or is it redundant?

1
  • For projects without an ORM, we have some toDB and fromDB static methods on the DTOs. For projects that use an ORM, we have toDTO and fromDTO on the Object classes Commented May 21 at 9:09

2 Answers 2

2

You're thinking in the right direction. Most tutorials show how to use DTOs (Data Transfer Objects) for handling requests and responses — like between the frontend and the API.

But what about data going between your app and the database? That’s also important — and many real projects handle it using different DTOs or mappers.

When reading from DB:

const user = await this.userRepository.findOne({where: {"user_id": 1} })

return new User(
  user.user_id,
  user.first_name,
  user.last_name
)

When saving to DB:

const user = new User();

user.user_id = data.user_id;
user.first_name = data.first_name;
user.last_name = data.last_name; 

await this.userRepository.save(user)

Note

You can use some function defined in class which accept some arguments and return filtered value;

Sign up to request clarification or add additional context in comments.

Comments

1

I agree with @Ashish Srivastava’s answer — especially in typical NestJS apps — but I'd like to expand on it by considering deeper architectural layers, particularly around the domain–infrastructure boundary.

How you handle DTOs really depends on how many layers exist between your controllers and your persistence layer, and how these layers are allowed to interact (e.g., whether one module can access another’s domain models, which is commonly allowed but still worth considering carefully).

Coming from a C# background (where architecture patterns like Clean Architecture or Onion Architecture are common), I've found this rule of thumb to work well — and it's very applicable to NestJS/TypeScript too:

  • Use DTOs between the Controller and Service layers — this is your Presentation Layer, and DTOs here serve as API contracts (often used with validation decorators like class-validator).

  • If you have additional layers between Service and your Domain Model (like an Application Service or Command Handler), you might introduce other types, but these aren’t always called "DTOs". You might call them things like CreateUserResult or UserData, defined as simple classes or interfaces.

  • This allows other modules to access these interfaces or types without tightly coupling to the HTTP-specific DTOs.

  • For mapping, I usually use static methods in the DTO or Mapper classes, like fromEntity, toEntity, fromDomain, toDomain, etc. This makes transformation and encapsulation explicit and testable.

This pattern keeps your application modular, decoupled, and easier to test or evolve later (e.g., if you change the persistence mechanism, or expose the domain logic via GraphQL or gRPC in the future).

Architectural layers:

Controller (REST layer)
  ↓
Service (Application logic)
  ↓
Domain (Business logic / core models)
  ↓
Repository (Persistence abstraction)
  ↓
ORM Entity (Database representation)

UserEntity or UserModel

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity('users')
export class UserEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  username: string;

  @Column()
  email: string;
}

UserDomain

export class User {
  constructor(
    public readonly id: string,
    public username: string,
    public email: string,
  ) {}

  changeEmail(newEmail: string) {
    this.email = newEmail;
  }
}

UserMapper

export class UserMapper {
  static toDomain(entity: UserEntity): User {
    return new User(entity.id, entity.username, entity.email);
  }

  static toEntity(domain: User): UserEntity {
    const entity = new UserEntity();
    entity.id = domain.id;
    entity.username = domain.username;
    entity.email = domain.email;
    return entity;
  }
}

UserDto

import { User } from '@/domain/user/user';

export class UserResponseDto {
  id: string;
  username: string;
  email: string;

  static fromDomain(user: User): UserResponseDto {
    const dto = new UserResponseDto();
    dto.id = user.id;
    dto.username = user.username;
    dto.email = user.email;
    return dto;
  }
}

This example give you a flexibility on how many layers you want to implement, how they are interacted with each other and how to manage a specified layer logic.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.