Spring Boot Best Practices: Use DTOs Instead of Entities in API Responses

When building REST APIs in Spring Boot, many developers directly return JPA entities in API responses.

This may seem easy, but it’s a bad practice because it can:
Expose sensitive fields (Security risk)
Cause lazy loading issues (Performance impact)
Tightly couple database structure with the API

💡 Solution? Use DTOs (Data Transfer Objects) instead of entities in responses.

In this guide, you'll learn:
✅ Why exposing JPA entities is dangerous
✅ How DTOs improve API security and performance
✅ How to combine multiple entities into a single DTO
✅ A complete Spring Boot example with best practices

🚨 Problem: Exposing Entities Can Leak Sensitive Data

Let's say we have a User entity representing database records:

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String email;
    private String password; // 🚨 Sensitive data
    private LocalDateTime createdAt;

    // Getters and Setters
}

❌ Bad Practice: Returning Entities in API Response

If we expose the entity directly in a REST API, the response might look like this:

📌 GET /api/users Response:

{
    "id": 1,
    "username": "rajiv",
    "email": "rajiv@example.com",
    "password": "hashed_password_123",  // 🚨 Exposing sensitive data
    "createdAt": "2024-02-25T10:00:00"
}

Problem: The API leaks password and createdAt, which should not be visible to users!

✅ Solution: Use DTOs to Hide Sensitive Fields

Instead of exposing the full entity, we create a DTO that only includes necessary fields.

public record UserDTO(Long id, String username, String email) { }

✔ Good Practice: Returning DTO Instead of Entity

📌 GET /api/users Response (Using DTOs):

{
    "id": 1,
    "username": "rajiv",
    "email": "rajiv@example.com"
}

No password or unnecessary fields
Secure and optimized response

🔄 Advanced Use Case: Merging Data from Two Entities into a DTO

In real-world applications, we often need to fetch data from multiple tables.
Instead of making two separate API calls, we can merge fields from multiple entities into a single DTO.

Example: User + Address Relationship

@Entity
@Table(name = "addresses")
public class Address {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String street;
    private String city;
    private String country;

    @OneToOne
    @JoinColumn(name = "user_id")
    private User user;
}

✅ Create a DTO That Merges User & Address

We can create a DTO that combines user details and address details into a single response.

public record UserAddressDTO(
        Long id, 
        String username, 
        String email, 
        String street, 
        String city, 
        String country) { }

🔄 Convert Entities to DTO in the Service Layer

@Service
public class UserService {
    
    private final UserRepository userRepository;
    private final AddressRepository addressRepository;

    public UserService(UserRepository userRepository, AddressRepository addressRepository) {
        this.userRepository = userRepository;
        this.addressRepository = addressRepository;
    }

    // Convert User & Address to a single DTO
    private UserAddressDTO mapToDTO(User user, Address address) {
        return new UserAddressDTO(
                user.getId(),
                user.getUsername(),
                user.getEmail(),
                address.getStreet(),
                address.getCity(),
                address.getCountry()
        );
    }

    public UserAddressDTO getUserWithAddress(Long userId) {
        User user = userRepository.findById(userId)
                                  .orElseThrow(() -> new RuntimeException("User not found"));
        Address address = addressRepository.findByUser(user)
                                           .orElseThrow(() -> new RuntimeException("Address not found"));

        return mapToDTO(user, address);
    }
}

🖥️ Create a Controller to Return the Merged DTO

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{id}/details")
    public ResponseEntity<UserAddressDTO> getUserWithAddress(@PathVariable Long id) {
        return ResponseEntity.ok(userService.getUserWithAddress(id));
    }
}

✅ Optimized API Response (Merged DTO)

📌 GET /api/users/1/details Response:

{
    "id": 1,
    "username": "rajiv",
    "email": "rajiv@example.com",
    "street": "123 Main St",
    "city": "Mumbai",
    "country": "India"
}

🚀 Key Takeaways

NEVER expose JPA entities in API responses
✅ Use DTOs (Java Records) to control API responses
✅ Use DTOs to merge multiple entities into a single response
✅ Reduce unnecessary API calls by sending combined data in one request

By following these best practices, your Spring Boot APIs will be more secure, maintainable, and optimized for performance. 🚀

💡 Next Steps

🔥 Optimize DTO Mapping → Use MapStruct for automatic DTO conversion
🔥 Secure Your API → Implement Spring Security for authentication
🔥 Boost API Performance → Use Spring Boot Caching

🔹 Enjoyed this article? Share it and help more developers write better Spring Boot APIs! 🚀

Comments

Spring Boot 3 Paid Course Published for Free
on my Java Guides YouTube Channel

Subscribe to my YouTube Channel (165K+ subscribers):
Java Guides Channel

Top 10 My Udemy Courses with Huge Discount:
Udemy Courses - Ramesh Fadatare