API Simplicity Showdown: Spring Data REST vs. DTOs
- Mark Kendall
- Mar 14
- 5 min read
API Simplicity Showdown: Spring Data REST vs. DTOs
When building REST APIs, developers often face the decision of how to represent and manage data. Two popular approaches are Spring Data REST and Data Transfer Objects (DTOs). This article compares these two methods using a simple example involving customers, tables, and accounts.
Scenario:
We need to create REST endpoints for managing customers, tables, and accounts. Each entity has basic fields, and customers have a one-to-many relationship with accounts.
Spring Data REST Approach:
Spring Data REST leverages Spring Data JPA to automatically expose JPA entities as REST resources.
Entities (Shared for Both Approaches):
```java
import javax.persistence.*;
import java.util.List;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
@OneToMany(mappedBy = "customer")
private List<Account> accounts;
// Getters and setters...
}
@Entity
public class Table {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private int tableNumber;
private int capacity;
// Getters and setters...
}
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String accountNumber;
private double balance;
@ManyToOne
@JoinColumn(name = "customer_id")
private Customer customer;
// Getters and setters...
}
```
Repositories (Spring Data REST):
```java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource
public interface CustomerRepository extends JpaRepository<Customer, Long> {}
@RepositoryRestResource
public interface TableRepository extends JpaRepository<Table, Long> {}
@RepositoryRestResource
public interface AccountRepository extends JpaRepository<Account, Long> {}
```
Results (Spring Data REST):
* Minimal code required.
* Automatic CRUD endpoints.
* HATEOAS links for discoverability.
* Relationship management.
* Quick setup.
DTO Approach:
The DTO approach involves creating separate classes to represent the data transferred between the client and server.
DTOs:
```java
public class CustomerDTO {
private Long id;
private String firstName;
private String lastName;
private String email;
// Getters and setters...
}
public class TableDTO {
private Long id;
private int tableNumber;
private int capacity;
// Getters and setters...
}
public class AccountDTO {
private Long id;
private String accountNumber;
private double balance;
private Long customerId;
// Getters and setters...
}
```
Repositories (DTO Approach):
```java
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {}
@Repository
public interface TableRepository extends JpaRepository<Table, Long> {}
@Repository
public interface AccountRepository extends JpaRepository<Account, Long> {}
```
Controllers (DTO Approach):
```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api")
public class ApiController {
@Autowired
private CustomerRepository customerRepository;
@Autowired
private TableRepository tableRepository;
@Autowired
private AccountRepository accountRepository;
@GetMapping("/customers")
public List<CustomerDTO> getCustomers() {
return customerRepository.findAll().stream()
.map(this::convertToCustomerDTO)
.collect(Collectors.toList());
}
@PostMapping("/customers")
public CustomerDTO createCustomer(@RequestBody CustomerDTO customerDTO) {
Customer customer = convertToCustomerEntity(customerDTO);
customer = customerRepository.save(customer);
return convertToCustomerDTO(customer);
}
@GetMapping("/tables")
public List<TableDTO> getTables() {
return tableRepository.findAll().stream().map(this::convertToTableDTO).collect(Collectors.toList());
}
@PostMapping("/tables")
public TableDTO createTable(@RequestBody TableDTO tableDTO) {
Table table = convertToTableEntity(tableDTO);
table = tableRepository.save(table);
return convertToTableDTO(table);
}
@GetMapping("/accounts")
public List<AccountDTO> getAccounts() {
return accountRepository.findAll().stream().map(this::convertToAccountDTO).collect(Collectors.toList());
}
@PostMapping("/accounts")
public AccountDTO createAccount(@RequestBody AccountDTO accountDTO) {
Account account = convertToAccountEntity(accountDTO);
Customer customer = customerRepository.findById(accountDTO.getCustomerId()).orElse(null);
account.setCustomer(customer);
account = accountRepository.save(account);
return convertToAccountDTO(account);
}
private CustomerDTO convertToCustomerDTO(Customer customer) {
CustomerDTO dto = new CustomerDTO();
dto.setId(customer.getId());
dto.setFirstName(customer.getFirstName());
dto.setLastName(customer.getLastName());
dto.setEmail(customer.getEmail());
return dto;
}
private Customer convertToCustomerEntity(CustomerDTO dto) {
Customer customer = new Customer();
customer.setId(dto.getId());
customer.setFirstName(dto.getFirstName());
customer.setLastName(dto.getLastName());
customer.setEmail(dto.getEmail());
return customer;
}
private TableDTO convertToTableDTO(Table table) {
TableDTO dto = new TableDTO();
dto.setId(table.getId());
dto.setTableNumber(table.getTableNumber());
dto.setCapacity(table.getCapacity());
return dto;
}
private Table convertToTableEntity(TableDTO dto) {
Table table = new Table();
table.setId(dto.getId());
table.setTableNumber(dto.getTableNumber());
table.setCapacity(dto.getCapacity());
return table;
}
private AccountDTO convertToAccountDTO(Account account) {
AccountDTO dto = new AccountDTO();
dto.setId(account.getId());
dto.setAccountNumber(account.getAccountNumber());
dto.setBalance(account.getBalance());
if(account.getCustomer() != null){
dto.setCustomerId(account.getCustomer().getId());
}
return dto;
}
private Account convertToAccountEntity(AccountDTO dto) {
Account account = new Account();
account.setId(dto.getId());
account.setAccountNumber(dto.getAccountNumber());
account.setBalance(dto.getBalance());
return account;
}
}
```
Results (DTO Approach):
* More code required.
* Explicit data mapping between entities and DTOs.
* Fine-grained control over API responses.
* No automatic HATEOAS.
Comparison:
| Feature | Spring Data REST | DTOs |
| ---------------- | ---------------- | --------------------- |
| Code Volume | Minimal | Higher |
| Setup Time | Faster | Slower |
| Control | Less | More |
| HATEOAS | Automatic | Manual |
| Relationship Handling | Automatic | Manual or with libraries|
| Use Cases | Simple CRUD APIs | Complex APIs, data transformation |
Conclusion:
Spring Data REST excels in simplicity and rapid development for basic CRUD APIs. It reduces boilerplate code and automatically handles many common tasks. However, it offers less control over API responses.
DTOs provide fine-grained control and flexibility, making them suitable for complex APIs with specific data transformation requirements. However, they require more code and manual mapping.
For simple applications, Spring Data REST can significantly reduce development time. For applications with complex business logic or strict API design requirements, DTOs offer the necessary flexibility.
Comments