Using DTOs in a Spring REST API

Introduction

In this tutorial, we will introduce the Data Transfer Object pattern (DTOs) in order to increase the efficiency and security of transferring data between layers of our REST API.

What do we create?

A DTO (Data Transfer Object) is a simple object used to transfer data between different layers or parts of a software application. It is an object created by attributes from different entities or by fewer attributes than the actual model.

Usually, when working with models stored in databases, we have additional fields which we generate when we are creating a model. 

For instance, we can leave the responsibility of generating the ID to the storage layer and avoid passing it when creating a new instance of an entity. Additionally, we might have other attributes, such as createDate or lastUpdateDate, to store information about the entity.

DTOs allow you to control exactly which fields of your domain models are exposed to the API consumers. This minimizes the risk of unintentionally exposing sensitive information and they decouple the internal representation of your business entities from the external API contract.

In this tutorial we will see how to integrate the DTO pattern in our REST API application created with Spring Framework.

How do we create it?

You can use Spring Initializr to quickly generate a Spring Boot project with Maven or Gradle. We can use the following configurations:

  • Group: com.example
  • Artifact: explainjavaRestAPI
  • Packing: jar
  • Java version: any
  • Dependencies: Spring Web (to create a REST API)
Spring REST API Initializr configuration

You will receive a ready-to-use project. All the configurations are already written in the build.gradle file and the start ExplainjavaRestApiApplication class is already created.

The goal of this tutorial is to create a REST API application to serve the resource “Lemonade” to clients. In order to do that, we will create a multi-layer application with an in-memory repository for storage and annotations based dependency injection:

Domain: Lemonade
				
					public class Lemonade {

    private Long id;
    private String name;
    private String description;

    public Lemonade() {
    }

    public Lemonade(String name, String description) {
        this.name = name;
        this.description = description;
    }
    
    // getters and setters 
    
}
				
			
				
					@Repository
public class LemonadeRepository {

    private final Map<Long, Lemonade> storage = new HashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    public Lemonade save(Lemonade lemonade) {
        if (lemonade.getId() == null) {
            lemonade.setId(idGenerator.getAndIncrement());
        }
        storage.put(lemonade.getId(), lemonade);
        return lemonade;
    }

    public Lemonade update(Long id, Lemonade updatedLemonade) {
        if (storage.containsKey(id)) {
            updatedLemonade.setId(id);
            storage.put(id, updatedLemonade);
            return updatedLemonade;
        }

        return null;
    }

    public Optional<Lemonade> findById(Long id) {
        return Optional.ofNullable(storage.get(id));
    }

    public List<Lemonade> findAll() {
        return new ArrayList<>(storage.values());
    }

    public boolean deleteById(Long id) {
        return storage.remove(id) != null;
    }
}
				
			
				
					@Service
public class LemonadeService {

    @Autowired
    private LemonadeRepository lemonadeRepository;

    public Lemonade createLemonade(Lemonade lemonade) {
        return lemonadeRepository.save(lemonade);
    }

    public List<Lemonade> getAllLemonades() {
        return lemonadeRepository.findAll();
    }

}
				
			

If we take a look closer, when we are saving an entity, we generate the ID:

				
					@Repository
public class LemonadeRepository {

    private final Map<Long, Lemonade> storage = new HashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    public Lemonade save(Lemonade lemonade) {
        if (lemonade.getId() == null) {
            lemonade.setId(idGenerator.getAndIncrement());
        }
        storage.put(lemonade.getId(), lemonade);
        return lemonade;
    }
    
    // the rest of the CRUD operations
    
}
				
			

That means, when we are creating an entity, we don’t have to specify the ID. We need to pass only the name and the description of the Lemonade. We can create a new class for that (you can check our tutorial on using Records or the Lombok Project to reduce the boilerplate code for these types of classes).

				
					public class LemonadeDTO {

    private String name;
    private String description;

    public LemonadeDTO() {
    }

    public LemonadeDTO(String name, String description) {
        this.name = name;
        this.description = description;
    }

   // getters and setters
}
				
			

Additionally, we need to add a specialized class to convert the DTO from model and the model from DTO. We can create a package called “converter” to add the DTO and the converter class.

				
					public class LemonadeConverter {

    public static Lemonade convertFromDTO(LemonadeDTO lemonadeDTO){
        return new Lemonade(lemonadeDTO.getName(), lemonadeDTO.getDescription());
    }

    public static LemonadeDTO convertToDTO(Lemonade lemonade){
        return new LemonadeDTO(lemonade.getName(), lemonade.getDescription());
    }
}
				
			

Finally, we can add the Controller class, and we can use the newly created classes. 

First, we add the LemonadeDTO as expected body for the POST operation, then, we convert the responses to DTO classes to decouple the internal representation of our entities from the external API.

				
					@RestController
@RequestMapping("/api/lemonades")
public class LemonadeController {

    @Autowired
    private LemonadeService lemonadeService;

    @PostMapping
    public LemonadeDTO createLemonade(@RequestBody LemonadeDTO lemonadeDTO) {
        Lemonade lemonade = lemonadeService.createLemonade(LemonadeConverter.convertFromDTO(lemonadeDTO));
        return LemonadeConverter.convertToDTO(lemonade);
    }

    @GetMapping
    public List<LemonadeDTO> getAllLemonades() {
        return lemonadeService.getAllLemonades().stream().map(LemonadeConverter::convertToDTO).collect(Collectors.toList());
    }


}
				
			

Now, when we create a test using Postman, we don’t need to pass the ID of the entity and we let the storage layer handle it.

Testing DTO REST API with Postman

Conclusion

In this tutorial, we discussed about integrating DTO pattern in a basic REST API created with Spring Framework.