Refactoring Your API with DTOs

Refactoring Your API with DTOs

Have you ever exposed a database entity in your API response, only to realize later that you had an API break because of a database change? The DTO pattern helps solve these problems."

The DTO pattern is the use of a data object for sending and retrieving information in a concise, decoupled, and flexible way. This pattern is typically used to avoid exposing Data Model entities on Rest APIs or to transfer objects over the network when using microservices.

The main pillar that this pattern supports is Encapsulation.

When I first saw a DTO, my question was: Why not just expose the Data Model Entities directly?"

  • You might unintentionally expose sensitive data, such as internal IDs, passwords, or implementation details that should remain private.

  • Changing the data model might break existing API clients.

Key Concepts of DTO Pattern

  1. Encapsulation of Data – DTOs hold only data, without business logic.

  2. Reducing Overfetching and Underfetching – They help avoid exposing unnecessary fields or missing required ones.

  3. Decoupling from Entities – DTOs prevent direct exposure of database entities.

  4. Improved Performance – They reduce serialization overhead when transferring data over a network.

Based on this, what are the advantages?

  • Easy to follow a contract-first approach: Using Dtos makes it easier to follow a Contract-first approach (designing your API before writing any backend logic)

  • Better API Design – Clear separation between database models and API responses.

  • Safer Code – Avoids exposing unnecessary or sensitive data.

  • Easier to Change – We can modify the entity without breaking API consumers.

  • More Scalable – Works well as the project grows (pagination, filtering, etc.).

Let’s see a simple example of how the DTO pattern works in a Spring Boot application:

1. Entity Model (JPA)

@Entity
@Table(name = "posts")

public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String title;
    private String content;
    private LocalDateTime createdAt;

    // Getters and setters
}

2. DTO Class

public class PostDTO {
    private Long id;
    private String title;
    public PostDTO(Long id, String title) {
        this.id = id;
        this.title = title;
    }
}

3. Service Layer (Converting Entity to DTO)

@Service
public class PostService {
    @Autowired
    private PostRepository postRepository;

    public List<PostDTO> getAllPosts() {
        return postRepository.findAll().stream()
                .map(post -> new PostDTO(post.getId(), post.getTitle()))
                .collect(Collectors.toList());

    }
}

4. Controller Layer:

@RestController

@RequestMapping("/posts")

public class PostController {
    @Autowired
    private PostService postService;

    @GetMapping
    public List<PostDTO> getPosts() {
        return postService.getAllPosts();
    }
}

"By implementing the DTO pattern, I made this demo application more robust, scalable, and secure. You can check out the practical changes I made here: [GitHub link]."https://github.com/mdjc/blog-posts-app/commit/50111a536391b7a630d5e4eeb0b7349850fed50e